From f071dbaad00762c28331a497662e7e5e2a8c210e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Sun, 26 Jul 2026 11:34:36 +0900 Subject: [PATCH 1/4] test: lock v2 protocol conformance --- .../standard/extension-delegation-standard.md | 33 ++- docs/standard/v2-projection-profile.md | 16 +- docs/standard/v2-public-surface.json | 12 +- packages/json-document/README.md | 4 + .../src/domain/projection/create.ts | 13 +- .../tests/conformance/v2/jsonpath-suite.ts | 167 +++++++++++ .../tests/conformance/v2/pointer-suite.ts | 112 ++++++++ .../tests/conformance/v2/pointer-vectors.json | 130 +++++++++ .../tests/conformance/v2/projection-suite.ts | 158 ++++++++++- .../conformance/v2/projection-vectors.json | 265 ++++++++++++++++++ .../tests/conformance/v2/rfc6902-suite.ts | 80 ++++++ .../v2-jsonpath-standard-conformance.test.ts | 12 + .../v2-pointer-standard-conformance.test.ts | 24 ++ ...v2-projection-standard-conformance.test.ts | 51 +++- .../v2-rfc6902-standard-conformance.test.ts | 12 + scripts/evaluate-standardization.mjs | 136 ++++++++- 16 files changed, 1198 insertions(+), 27 deletions(-) create mode 100644 packages/json-document/tests/conformance/v2/jsonpath-suite.ts create mode 100644 packages/json-document/tests/conformance/v2/pointer-suite.ts create mode 100644 packages/json-document/tests/conformance/v2/pointer-vectors.json create mode 100644 packages/json-document/tests/conformance/v2/rfc6902-suite.ts create mode 100644 packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts create mode 100644 packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts create mode 100644 packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts diff --git a/docs/standard/extension-delegation-standard.md b/docs/standard/extension-delegation-standard.md index 1f5e091f..0f7b73d3 100644 --- a/docs/standard/extension-delegation-standard.md +++ b/docs/standard/extension-delegation-standard.md @@ -1,6 +1,12 @@ # json-document extension delegation standard -상태: 표준화 트랙 초안. +상태: 1.x Candidate Editing Session migration baseline. v2 portable root에는 +비정본이다. + +이 문서의 selection, clipboard, history와 `selectionAfter` 서술은 +`@interactive-os/json-document/session` 후보 표면을 설명한다. v2 root의 규범 +계약은 [`v2-projection-profile.md`](./v2-projection-profile.md)가 우선하며, +v2 extension은 여섯-member `JSONDocument`와 JSON-safe metadata를 조합해야 한다. 이 문서는 `@interactive-os/json-document` official extension이 무엇이어야 하는지 정의한다. 핵심은 패키지 수를 늘리는 것이 아니다. `@interactive-os/json-document`의 목표는 FE 편집 도구에서 @@ -18,9 +24,10 @@ official extension ## 1. 철학 -`@interactive-os/json-document` core는 primitive foundation이다. core는 schema로 보호되는 JSON +`@interactive-os/json-document/session`은 1.x rich foundation이다. 이 Session은 schema로 보호되는 JSON document, pointer, patch, query, selection, clipboard, history, capability를 -제공한다. core는 UI, DOM, keyboard, focus, storage lifecycle, system +제공한다. v2 root Core는 JSON, Pointer, Patch, query와 여섯-member Projection만 +필수로 제공한다. 둘 다 UI, DOM, keyboard, focus, storage lifecycle, system clipboard permission, product command naming을 소유하지 않는다. extension은 core보다 편한 wrapper가 아니다. extension은 app과 core 사이에서 @@ -251,7 +258,12 @@ extension은 feature의 절차를 충분히 소유해야 한다. extension은 core implementation detail에 기대면 안 된다. -- MUST: `@interactive-os/json-document` public package entrypoint만 import해야 한다. +- MUST: 대상으로 삼은 profile의 public package entrypoint만 import해야 한다. + v2 portable extension은 `@interactive-os/json-document` root를 사용하고, 아직 + migration 중인 1.x Session extension은 + `@interactive-os/json-document/session`을 사용할 수 있다. +- MUST: `/session`에 의존한 extension을 여섯-member v2 Core 적합 구현으로 + 표시하면 안 된다. - MUST NOT: `src/application`, `src/domain`, `src/foundation` 같은 internal path를 import하면 안 된다. - MUST NOT: private symbol, test helper, implementation-only type을 public contract처럼 사용하면 안 된다. - SHOULD: extension evaluator로 import boundary를 검사해야 한다. @@ -281,7 +293,10 @@ extension은 필요할 때 함수로 조립한다. 다만 official extension은 구조 편집 command는 JSON document의 모양, sibling order, group, outline, range, grid-like region을 바꾼다. 이 계열은 core API를 새로 요구하지 않는다. -Core에는 이미 JSON Patch batch와 `commit(..., { selectionAfter })`가 있다. +1.x Candidate Session에는 JSON Patch batch와 +`commit(..., { selectionAfter })`가 있다. v2 root Core는 +`commit(operations, { metadata })`만 제공하므로 v2 extension의 +`selectionAfter`는 extension 또는 rich host가 해석하는 JSON-safe metadata다. 따라서 extension은 같은 의미를 새 이름으로 다시 만들지 말고, 다음 어휘로 수렴해야 한다. @@ -311,12 +326,16 @@ structural command |-- plan/can 단계에서 operations를 노출한다 |-- command가 다음 focus를 알면 selectionAfter를 노출한다 |-- 실행을 막지 않는 정보는 diagnostics에 둔다 -`-- 실제 commit은 core doc.commit(..., { selectionAfter })를 사용한다 +`-- v2 root commit은 selectionAfter를 namespaced metadata로 전달한다 ``` `selectionAfter`는 DOM focus가 아니다. JSON Pointer, selection range input, 또는 `SelectionSnap`으로 복원할 수 있는 headless selection이다. Visual focus, keyboard roving tabindex, scroll position은 host app 책임으로 남는다. +위 command result의 `unknown`은 Session별 selection model을 허용하는 local +boundary다. v2 root metadata로 넘길 때에는 extension 또는 host가 이를 +`JSONValue`로 encode하고 자기 namespace 아래에 넣어야 하며, core가 +`selectionAfter`라는 예약 field를 해석한다고 가정하면 안 된다. `diagnostics`는 실패 result를 대체하지 않는다. 실행을 막는 문제는 stable `code`를 가진 실패 result여야 한다. `diagnostics`는 import/paste/review처럼 host가 @@ -700,7 +719,7 @@ official 승격 전에는 다음 표를 작성해야 한다. | Residual app responsibility | 앱에 남아야 하는 UX/product 책임은 무엇인가? | | Cross-app evidence | outliner 외 어느 편집도구에서도 같은 이름으로 필요한가? | | can/execute parity | `can*`와 실행 method가 같은 feature semantics를 공유하는가? | -| Public facade | public `@interactive-os/json-document` entrypoint만으로 구현되는가? | +| Public facade | target profile의 public entrypoint만으로 구현되는가? | | Non-goals | UI, focus, keyboard, product workflow를 명시적으로 제외했는가? | | Failure model | structured result와 error code가 충분한가? | | Tests | feature 규칙이 app test가 아니라 extension test에 있는가? | diff --git a/docs/standard/v2-projection-profile.md b/docs/standard/v2-projection-profile.md index 8e2ea4a7..216c5279 100644 --- a/docs/standard/v2-projection-profile.md +++ b/docs/standard/v2-projection-profile.md @@ -72,7 +72,7 @@ JSON이 아니거나 acceptance에 거부되면 TypeScript reference binding은 | JD2-CAPABILITY-001 | `canPatch`는 state와 subscriber를 바꾸지 않고 `commit`과 같은 JSON, Pointer, Patch, schema acceptance 의미를 사용해야 한다. state와 acceptance rule이 사이에 변하지 않았다면 성공한 probe와 같은 입력의 commit이 같은 검증 원인으로 실패하면 안 된다. | | JD2-COMMIT-001 | `commit`은 Projection의 유일한 stateful mutation primitive여야 하고 local operation을 동기적·원자적으로 publish해야 한다. expected input failure는 throw나 rejected Promise가 아니라 result이며 partial state나 partial applied patch를 노출하면 안 된다. | | JD2-COMMIT-002 | 성공한 `commit`은 `JSONAppliedChange`를 담은 `JSONDocumentCommitResult`를 반환해야 한다. 그 change는 실제 적용된 canonical operation과 JSON-safe metadata만 포함하며 post-commit value는 `value`에서 읽는다. | -| JD2-PUBLISH-001 | state-changing commit은 같은 인과 순서의 operation과 metadata를 가진 value-equivalent `JSONAppliedChange`를 subscriber마다 정확히 한 번 전달해야 한다. object identity는 계약이 아니다. 재진입 commit도 모든 subscriber에게 같은 순서로 전달해야 하며 한 listener의 예외가 다른 listener 전달을 막으면 안 된다. 실패와 state-equivalent no-op은 notification을 만들면 안 되며 unsubscribe 이후에는 notification을 전달하면 안 된다. | +| JD2-PUBLISH-001 | state-changing commit은 같은 인과 순서의 operation과 metadata를 가진 value-equivalent `JSONAppliedChange`를 subscriber마다 정확히 한 번 전달해야 한다. object identity는 계약이 아니다. 재진입 commit도 모든 subscriber에게 같은 순서로 전달해야 한다. 한 listener의 예외는 다른 listener 전달을 막거나 `commit` 밖으로 전파되거나 성공 result를 바꾸면 안 된다. 실패와 state-equivalent no-op은 notification을 만들면 안 되며 unsubscribe 이후에는 notification을 전달하면 안 된다. | | JD2-RESULT-001 | public result는 boolean `ok` discriminant를 가져야 하고 실패는 stable string `code`를 가져야 한다. expected read, query, capability, commit failure를 예외로만 표현해서는 안 된다. | | JD2-RESULT-002 | consumer는 모르는 result field와 error code를 일반적으로 처리할 수 있어야 한다. minor release는 기존 required field나 code의 의미를 바꾸면 안 되지만 optional field와 새 code는 MAY 추가할 수 있으므로 exact `Object.keys` 집합은 계약이 아니다. | | JD2-SCHEMA-001 | Projection이 acceptance rule을 구성하면 initial state와 commit candidate를 publish 전에 검사해야 한다. 이 boundary는 provider-neutral이어야 하며 `_zod`, provider issue, private schema object를 Projection 적합성에 요구하면 안 된다. 제약 없는 acceptance는 MAY 허용한다. Acceptance callback은 같은 Projection의 `canPatch`나 `commit`을 재진입해서는 안 되며, binding은 그런 호출을 failure로 차단해 state와 publication을 보존해야 한다. | @@ -144,7 +144,8 @@ unknown code를 허용하는 forward-compatibility fixture를 포함한다. ## Conformance artifact -black-box suite는 Protocol과 Projection을 각각 세 책임으로 나눈다. +black-box suite는 여섯-member Projection, pure Patch, Pointer, 외부 RFC +conformance corpus의 public-root binding을 서로 분리한다. | Artifact | 책임 | | --- | --- | @@ -154,6 +155,13 @@ black-box suite는 Protocol과 Projection을 각각 세 책임으로 나눈다. | `packages/json-document/tests/conformance/v2/protocol-vectors.json` | schema-free pure Patch 입력과 기대값 | | `packages/json-document/tests/conformance/v2/protocol-suite.ts` | `applyPatch`만 아는 injected harness runner | | `packages/json-document/tests/public/v2-protocol-standard-conformance.test.ts` | public root pure Protocol binding | +| `packages/json-document/tests/conformance/v2/pointer-vectors.json` | RFC 6901 조합과 applied Patch 기반 pointer tracking 기대값 | +| `packages/json-document/tests/conformance/v2/pointer-suite.ts` | Pointer helper만 아는 injected harness runner | +| `packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts` | 여섯 public Pointer helper의 root binding | +| `packages/json-document/tests/conformance/v2/rfc6902-suite.ts` | vendored RFC 6902 corpus를 실행하는 provider-neutral runner | +| `packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts` | public root `applyPatch`의 전체 RFC 6902 corpus binding | +| `packages/json-document/tests/conformance/v2/jsonpath-suite.ts` | vendored RFC 9535 CTS를 `query`와 `at`으로 검증하는 runner | +| `packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts` | public root Projection의 전체 RFC 9535 CTS binding | suite가 export하는 structural type은 test harness 내부 계약이며 package public export가 아니다. @@ -181,7 +189,9 @@ stable이라고 선언할 수 없다. | Remove | 89 | 파생 alias, component result, 고급 selection, schema introspection, TextSurface | 139개 이름의 유일한 분류와 replacement는 -[`v2-public-surface.json`](./v2-public-surface.json)이 정본이다. +[`v2-public-surface.json`](./v2-public-surface.json)이 정본이다. 이 manifest의 +`sourceContract`는 현재 v2 root를, `migrationBaselineContract`는 disposition의 +출처인 1.x `/session`을 각각 가리킨다. 도입 이름은 `JSONValue`, `JSONAppliedChange`, `JSONPatchResult`, `JSONDocumentCommitResult`, `JSONDocumentPlacementTarget`, diff --git a/docs/standard/v2-public-surface.json b/docs/standard/v2-public-surface.json index b661f526..5a57abc1 100644 --- a/docs/standard/v2-public-surface.json +++ b/docs/standard/v2-public-surface.json @@ -1,14 +1,22 @@ { "formatVersion": 1, "status": "candidate", - "sourceContract": "packages/json-document/public-contract.json#session", + "sourceContract": "packages/json-document/public-contract.json#root", + "migrationBaselineContract": "packages/json-document/public-contract.json#session", "conformance": { "projectionVectors": "packages/json-document/tests/conformance/v2/projection-vectors.json", "projectionSuite": "packages/json-document/tests/conformance/v2/projection-suite.ts", "projectionBinding": "packages/json-document/tests/public/v2-projection-standard-conformance.test.ts", "protocolVectors": "packages/json-document/tests/conformance/v2/protocol-vectors.json", "protocolSuite": "packages/json-document/tests/conformance/v2/protocol-suite.ts", - "protocolBinding": "packages/json-document/tests/public/v2-protocol-standard-conformance.test.ts" + "protocolBinding": "packages/json-document/tests/public/v2-protocol-standard-conformance.test.ts", + "pointerVectors": "packages/json-document/tests/conformance/v2/pointer-vectors.json", + "pointerSuite": "packages/json-document/tests/conformance/v2/pointer-suite.ts", + "pointerBinding": "packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts", + "rfc6902Suite": "packages/json-document/tests/conformance/v2/rfc6902-suite.ts", + "rfc6902Binding": "packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts", + "jsonPathSuite": "packages/json-document/tests/conformance/v2/jsonpath-suite.ts", + "jsonPathBinding": "packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts" }, "requirements": [ "JD2-GOV-001", diff --git a/packages/json-document/README.md b/packages/json-document/README.md index 409bd699..380c1a48 100644 --- a/packages/json-document/README.md +++ b/packages/json-document/README.md @@ -76,6 +76,10 @@ Document의 필수 member는 여섯 개뿐입니다. | `commit(operations, options?)` | 유일한 stateful mutation | | `subscribe(listener)` | publish된 change 구독 | +subscriber는 state가 commit된 뒤 호출된다. 한 subscriber의 예외는 `commit` +밖으로 전파되거나 성공 result를 바꾸지 않으며, 뒤 subscriber의 전달도 막지 +않는다. + 실패는 throw 대신 `{ ok: false, code, reason?, pointer? }` result로 돌아옵니다. 새 error code와 optional field가 추가될 수 있으므로 consumer는 exact key 집합에 의존하지 않아야 합니다. diff --git a/packages/json-document/src/domain/projection/create.ts b/packages/json-document/src/domain/projection/create.ts index bb3adca6..222262fe 100644 --- a/packages/json-document/src/domain/projection/create.ts +++ b/packages/json-document/src/domain/projection/create.ts @@ -161,26 +161,23 @@ export function createProjection( if (publishing) return; publishing = true; - const errors: unknown[] = []; try { while (publicationQueue.length > 0) { const next = publicationQueue.shift() as JSONAppliedChange; for (const listener of [...listeners]) { + if (!listeners.has(listener)) continue; try { listener(next); - } catch (error) { - errors.push(error); + } catch { + // A subscriber runs after the state change is committed. Its failure + // cannot turn that successful commit into an apparent failed write, + // nor can it prevent delivery to the remaining subscribers. } } } } finally { publishing = false; } - - if (errors.length === 1) throw errors[0]; - if (errors.length > 1) { - throw new AggregateError(errors, "JSONDocument subscribers failed"); - } } } diff --git a/packages/json-document/tests/conformance/v2/jsonpath-suite.ts b/packages/json-document/tests/conformance/v2/jsonpath-suite.ts new file mode 100644 index 00000000..fc04f860 --- /dev/null +++ b/packages/json-document/tests/conformance/v2/jsonpath-suite.ts @@ -0,0 +1,167 @@ +import { describe, expect, test } from "vitest"; + +import cts from "../../public/jsonpath/conformance/jsonpath-cts.json" with { type: "json" }; +import type { JSONValue } from "./protocol-suite.js"; +import type { + ProjectionQueryResult, + ProjectionReadResult, +} from "./projection-suite.js"; + +interface JSONPathCase { + readonly name: string; + readonly selector: string; + readonly document?: JSONValue; + readonly result?: ReadonlyArray; + readonly results?: ReadonlyArray>; + readonly invalid_selector?: boolean; + readonly tags?: ReadonlyArray; +} + +interface JSONPathFailure { + readonly name: string; + readonly selector: string; + readonly reason: string; + readonly tags?: ReadonlyArray; +} + +interface JSONPathProjection { + at(pointer: string): ProjectionReadResult; + query(jsonPath: string): ProjectionQueryResult; +} + +export interface JSONPathHarness { + create(initial: JSONValue): JSONPathProjection; +} + +const EXPECTED_TOTAL = 703; +const EXPECTED_INVALID_SELECTORS = 247; +const suite = cts as unknown as { readonly tests: ReadonlyArray }; +const pointerCases = [ + { + name: "root query normalizes to the root Pointer", + selector: "$", + document: { value: "same" }, + pointers: [""], + }, + { + name: "equal values retain distinct array addresses", + selector: "$.items[*].value", + document: { + items: [{ value: "same" }, { value: "same" }], + }, + pointers: ["/items/0/value", "/items/1/value"], + }, + { + name: "object member names use RFC 6901 escaping", + selector: "$['a/b~c']", + document: { "a/b~c": "same" }, + pointers: ["/a~1b~0c"], + }, +] as const satisfies ReadonlyArray<{ + readonly name: string; + readonly selector: string; + readonly document: JSONValue; + readonly pointers: ReadonlyArray; +}>; + +export function runJSONPathConformance(harness: JSONPathHarness): void { + describe("json-document v2 RFC 9535 JSONPath CTS black-box conformance", () => { + test("full conformance does not regress", () => { + const failures: JSONPathFailure[] = []; + let invalidSelectors = 0; + let passed = 0; + + for (const testCase of suite.tests) { + const projection = harness.create( + "document" in testCase ? testCase.document ?? null : null, + ); + const queried = projection.query(testCase.selector); + if (testCase.invalid_selector) { + invalidSelectors += 1; + if (!queried.ok) { + passed += 1; + } else { + failures.push(failure(testCase, "expected invalid selector")); + } + continue; + } + + if (!queried.ok) { + failures.push(failure( + testCase, + queried.reason ?? "invalid query", + )); + continue; + } + + const result = queried.pointers.map((pointer) => { + const read = projection.at(pointer); + if (!read.ok) { + throw new Error(`query returned unreadable pointer: ${pointer}`); + } + return read.value; + }); + if (matchesAllowedResult(result, testCase)) { + passed += 1; + } else { + failures.push(failure(testCase, "result mismatch")); + } + } + + expect(suite.tests).toHaveLength(EXPECTED_TOTAL); + expect(invalidSelectors).toBe(EXPECTED_INVALID_SELECTORS); + expect( + passed, + JSON.stringify(failures.slice(0, 20), null, 2), + ).toBe(EXPECTED_TOTAL); + }); + + for (const pointerCase of pointerCases) { + test(pointerCase.name, () => { + const queried = harness.create(pointerCase.document).query( + pointerCase.selector, + ); + expect(queried.ok).toBe(true); + if (!queried.ok) return; + expect(queried.pointers).toEqual(pointerCase.pointers); + }); + } + }); +} + +function matchesAllowedResult( + actual: ReadonlyArray, + testCase: JSONPathCase, +): boolean { + const allowed = "result" in testCase + ? [testCase.result ?? []] + : testCase.results ?? []; + return allowed.some((expected) => sameJSON(actual, expected)); +} + +function failure( + testCase: JSONPathCase, + reason: string, +): JSONPathFailure { + return { + name: testCase.name, + selector: testCase.selector, + reason, + ...(testCase.tags === undefined ? {} : { tags: testCase.tags }), + }; +} + +function sameJSON(left: unknown, right: unknown): boolean { + return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); +} + +function canonical(value: unknown): unknown { + if (value === null || typeof value !== "object") return value; + if (Array.isArray(value)) return value.map(canonical); + const object = value as Record; + const normalized: Record = {}; + for (const key of Object.keys(object).sort()) { + normalized[key] = canonical(object[key]); + } + return normalized; +} diff --git a/packages/json-document/tests/conformance/v2/pointer-suite.ts b/packages/json-document/tests/conformance/v2/pointer-suite.ts new file mode 100644 index 00000000..da29bf0c --- /dev/null +++ b/packages/json-document/tests/conformance/v2/pointer-suite.ts @@ -0,0 +1,112 @@ +import { describe, expect, test } from "vitest"; + +import rawVectors from "./pointer-vectors.json" with { type: "json" }; +import type { JSONPatchOperation, JSONValue } from "./protocol-suite.js"; + +export interface PointerHarness { + parsePointer(pointer: string): string[]; + tryParsePointer(pointer: string): string[] | null; + buildPointer( + segments: ReadonlyArray, + options?: { readonly uriFragment?: boolean }, + ): string; + appendSegment(pointer: string, segment: string | number): string; + parentPointer(pointer: string): string | null; + trackPointer( + pointer: string, + applied: ReadonlyArray, + ): string | null; +} + +interface PointerManifest { + readonly parse: ReadonlyArray<{ + readonly id: string; + readonly pointer: string; + readonly segments: ReadonlyArray; + }>; + readonly invalid: ReadonlyArray<{ + readonly id: string; + readonly pointer: string; + }>; + readonly build: ReadonlyArray<{ + readonly id: string; + readonly segments: ReadonlyArray; + readonly pointer: string; + readonly fragment: string; + }>; + readonly append: ReadonlyArray<{ + readonly id: string; + readonly pointer: string; + readonly segment: string | number; + readonly expect: string; + }>; + readonly parent: ReadonlyArray<{ + readonly id: string; + readonly pointer: string; + readonly expect: string | null; + }>; + readonly track: ReadonlyArray<{ + readonly id: string; + readonly pointer: string; + readonly applied: ReadonlyArray; + readonly expect: string | null; + }>; +} + +const manifest = rawVectors as unknown as PointerManifest; + +export function runPointerConformance(harness: PointerHarness): void { + describe("json-document v2 Pointer black-box conformance", () => { + for (const vector of manifest.parse) { + test(`[${vector.id}]`, () => { + expect(harness.parsePointer(vector.pointer)).toEqual(vector.segments); + expect(harness.tryParsePointer(vector.pointer)).toEqual(vector.segments); + }); + } + + for (const vector of manifest.invalid) { + test(`[${vector.id}]`, () => { + expect(() => harness.parsePointer(vector.pointer)).toThrow(); + expect(harness.tryParsePointer(vector.pointer)).toBeNull(); + }); + } + + for (const vector of manifest.build) { + test(`[${vector.id}]`, () => { + expect(harness.buildPointer(vector.segments)).toBe(vector.pointer); + expect(harness.buildPointer(vector.segments, { + uriFragment: true, + })).toBe(vector.fragment); + }); + } + + for (const vector of manifest.append) { + test(`[${vector.id}]`, () => { + expect(harness.appendSegment(vector.pointer, vector.segment)).toBe( + vector.expect, + ); + }); + } + + for (const vector of manifest.parent) { + test(`[${vector.id}]`, () => { + expect(harness.parentPointer(vector.pointer)).toBe(vector.expect); + }); + } + + for (const vector of manifest.track) { + test(`[${vector.id}]`, () => { + expect(harness.trackPointer( + vector.pointer, + cloneJSON(vector.applied), + )).toBe(vector.expect); + }); + } + }); +} + +function cloneJSON>( + value: T, +): T { + return JSON.parse(JSON.stringify(value)) as T; +} diff --git a/packages/json-document/tests/conformance/v2/pointer-vectors.json b/packages/json-document/tests/conformance/v2/pointer-vectors.json new file mode 100644 index 00000000..2ff56f6a --- /dev/null +++ b/packages/json-document/tests/conformance/v2/pointer-vectors.json @@ -0,0 +1,130 @@ +{ + "formatVersion": 1, + "status": "candidate", + "profile": "docs/standard/v2-projection-profile.md", + "requirements": ["JD2-ADDRESS-001", "JD2-PATCH-001"], + "parse": [ + { "id": "pointer-root", "pointer": "", "segments": [] }, + { "id": "pointer-empty-member", "pointer": "/", "segments": [""] }, + { "id": "pointer-escaped-slash", "pointer": "/a~1b", "segments": ["a/b"] }, + { "id": "pointer-escaped-tilde", "pointer": "/m~0n", "segments": ["m~n"] }, + { "id": "pointer-uri-fragment", "pointer": "#/%ED%95%9C%EA%B8%80/%F0%9F%98%80", "segments": ["한글", "😀"] } + ], + "invalid": [ + { "id": "pointer-missing-root-marker", "pointer": "items/0" }, + { "id": "pointer-invalid-escape", "pointer": "/a~2b" }, + { "id": "pointer-invalid-fragment", "pointer": "#items/0" }, + { "id": "pointer-invalid-percent-encoding", "pointer": "#/%E0%A4%A" } + ], + "build": [ + { "id": "build-root", "segments": [], "pointer": "", "fragment": "#" }, + { "id": "build-escaped", "segments": ["a/b", "m~n"], "pointer": "/a~1b/m~0n", "fragment": "#/a~1b/m~0n" }, + { "id": "build-unicode", "segments": ["한글", "😀"], "pointer": "/한글/😀", "fragment": "#/%ED%95%9C%EA%B8%80/%F0%9F%98%80" }, + { "id": "build-number-segment", "segments": ["items", 0], "pointer": "/items/0", "fragment": "#/items/0" } + ], + "append": [ + { "id": "append-root", "pointer": "", "segment": "items", "expect": "/items" }, + { "id": "append-index", "pointer": "/items", "segment": 0, "expect": "/items/0" }, + { "id": "append-escaped", "pointer": "/items/0", "segment": "a/b~c", "expect": "/items/0/a~1b~0c" } + ], + "parent": [ + { "id": "parent-root", "pointer": "", "expect": null }, + { "id": "parent-top-level", "pointer": "/items", "expect": "" }, + { "id": "parent-nested", "pointer": "/items/0/title", "expect": "/items/0" } + ], + "track": [ + { + "id": "track-array-add", + "pointer": "/items/2/title", + "applied": [{ "op": "add", "path": "/items/0", "value": null }], + "expect": "/items/3/title" + }, + { + "id": "track-array-remove", + "pointer": "/items/3", + "applied": [{ "op": "remove", "path": "/items/1" }], + "expect": "/items/2" + }, + { + "id": "track-removed-descendant", + "pointer": "/items/1/title", + "applied": [{ "op": "remove", "path": "/items/1" }], + "expect": null + }, + { + "id": "track-replaced-target", + "pointer": "/title", + "applied": [{ "op": "replace", "path": "/title", "value": "Ready" }], + "expect": "/title" + }, + { + "id": "track-replaced-descendant", + "pointer": "/meta/owner", + "applied": [{ "op": "replace", "path": "/meta", "value": {} }], + "expect": null + }, + { + "id": "track-moved-descendant", + "pointer": "/items/0/title", + "applied": [{ "op": "move", "from": "/items/0", "path": "/archive/0" }], + "expect": "/archive/0/title" + }, + { + "id": "track-same-array-forward-moved-node", + "pointer": "/items/0/title", + "applied": [{ "op": "move", "from": "/items/0", "path": "/items/2" }], + "expect": "/items/2/title" + }, + { + "id": "track-same-array-forward-sibling", + "pointer": "/items/1/title", + "applied": [{ "op": "move", "from": "/items/0", "path": "/items/2" }], + "expect": "/items/0/title" + }, + { + "id": "track-same-array-backward-moved-node", + "pointer": "/items/2/title", + "applied": [{ "op": "move", "from": "/items/2", "path": "/items/0" }], + "expect": "/items/0/title" + }, + { + "id": "track-same-array-backward-sibling", + "pointer": "/items/0/title", + "applied": [{ "op": "move", "from": "/items/2", "path": "/items/0" }], + "expect": "/items/1/title" + }, + { + "id": "track-copy-shifts-array", + "pointer": "/items/2", + "applied": [{ "op": "copy", "from": "/template", "path": "/items/0" }], + "expect": "/items/3" + }, + { + "id": "track-ordered-batch", + "pointer": "/items/2", + "applied": [ + { "op": "add", "path": "/items/0", "value": null }, + { "op": "remove", "path": "/items/2" } + ], + "expect": "/items/2" + }, + { + "id": "track-root-replace", + "pointer": "/items/0", + "applied": [{ "op": "replace", "path": "", "value": null }], + "expect": null + }, + { + "id": "track-invalid-input", + "pointer": "items/0", + "applied": [{ "op": "test", "path": "", "value": null }], + "expect": null + }, + { + "id": "track-rejects-unresolved-append", + "pointer": "/items/0", + "applied": [{ "op": "move", "from": "/items/0", "path": "/items/-" }], + "expect": null + } + ] +} diff --git a/packages/json-document/tests/conformance/v2/projection-suite.ts b/packages/json-document/tests/conformance/v2/projection-suite.ts index 480ed294..4c2a60d1 100644 --- a/packages/json-document/tests/conformance/v2/projection-suite.ts +++ b/packages/json-document/tests/conformance/v2/projection-suite.ts @@ -133,6 +133,18 @@ interface UnsubscribeVector extends VectorBase { }; } +interface UnsubscribeDuringPublicationVector extends VectorBase { + readonly kind: "unsubscribe-during-publication"; + readonly operations: ReadonlyArray; + readonly metadata?: ProjectionMetadata; + readonly expect: { + readonly commit: ExpectedObject; + readonly firstSubscriber: ReadonlyArray; + readonly secondSubscriber: ReadonlyArray; + readonly value: JSONValue; + }; +} + interface MutationAttempt { readonly pointer: string; readonly value: JSONValue; @@ -173,13 +185,46 @@ interface NonJSONVector extends VectorBase { }; } +interface ReentrantPublicationVector extends VectorBase { + readonly kind: "reentrant-publication"; + readonly outer: { + readonly operations: ReadonlyArray; + readonly metadata?: ProjectionMetadata; + }; + readonly nested: { + readonly operations: ReadonlyArray; + readonly metadata?: ProjectionMetadata; + }; + readonly expect: { + readonly outer: ExpectedObject; + readonly nested: ExpectedObject; + readonly firstSubscriber: ReadonlyArray; + readonly secondSubscriber: ReadonlyArray; + readonly value: JSONValue; + }; +} + +interface SubscriberErrorVector extends VectorBase { + readonly kind: "subscriber-error"; + readonly operations: ReadonlyArray; + readonly metadata?: ProjectionMetadata; + readonly expect: { + readonly commit: ExpectedObject; + readonly delivered: ReadonlyArray; + readonly value: JSONValue; + }; +} + type ProjectionVector = | SurfaceVector | ReadVector | CommitVector | UnsubscribeVector + | UnsubscribeDuringPublicationVector | IsolationVector - | NonJSONVector; + | NonJSONVector + | ReentrantPublicationVector + | SubscriberErrorVector; interface ProjectionManifest { readonly initial: JSONValue; @@ -326,6 +371,34 @@ function runUnsubscribeVector( expectNotifications(observation.changes, vector.expect.notifications); } +function runUnsubscribeDuringPublicationVector( + harness: ProjectionHarness, + vector: UnsubscribeDuringPublicationVector, +): void { + const projection = harness.create("json", cloneJSON(manifest.initial)); + const firstSubscriber: ProjectionChange[] = []; + const secondSubscriber: ProjectionChange[] = []; + let unsubscribeSecond = (): void => undefined; + + projection.subscribe((change) => { + firstSubscriber.push(change); + unsubscribeSecond(); + }); + unsubscribeSecond = projection.subscribe((change) => { + secondSubscriber.push(change); + }); + + const result = projection.commit( + cloneJSON(vector.operations), + commitOptions(vector.metadata), + ); + + expectRequired(result, vector.expect.commit); + expectNotifications(firstSubscriber, vector.expect.firstSubscriber); + expectNotifications(secondSubscriber, vector.expect.secondSubscriber); + expect(projection.value).toEqual(vector.expect.value); +} + function runIsolationVector( harness: ProjectionHarness, vector: IsolationVector, @@ -388,6 +461,80 @@ function runNonJSONVector( expectNotifications(observation.changes, vector.expect.notifications); } +function commitOptions( + metadata: ProjectionMetadata | undefined, +): ProjectionCommitOptions | undefined { + return metadata === undefined + ? undefined + : { metadata: cloneJSON(metadata) }; +} + +function runReentrantPublicationVector( + harness: ProjectionHarness, + vector: ReentrantPublicationVector, +): void { + const projection = harness.create("json", cloneJSON(manifest.initial)); + const firstSubscriber: ProjectionChange[] = []; + const secondSubscriber: ProjectionChange[] = []; + let nestedResult: ProjectionCommitResult | undefined; + + projection.subscribe((change) => { + firstSubscriber.push(change); + if (firstSubscriber.length === 1) { + nestedResult = projection.commit( + cloneJSON(vector.nested.operations), + commitOptions(vector.nested.metadata), + ); + } + }); + projection.subscribe((change) => { + secondSubscriber.push(change); + }); + + const outerResult = projection.commit( + cloneJSON(vector.outer.operations), + commitOptions(vector.outer.metadata), + ); + + expectRequired(outerResult, vector.expect.outer); + expectRequired(nestedResult, vector.expect.nested); + expectNotifications( + firstSubscriber, + vector.expect.firstSubscriber, + ); + expectNotifications( + secondSubscriber, + vector.expect.secondSubscriber, + ); + expect(projection.value).toEqual(vector.expect.value); +} + +function runSubscriberErrorVector( + harness: ProjectionHarness, + vector: SubscriberErrorVector, +): void { + const projection = harness.create("json", cloneJSON(manifest.initial)); + const delivered: ProjectionChange[] = []; + let commitResult: ProjectionCommitResult | undefined; + + projection.subscribe(() => { + throw new Error("subscriber failed"); + }); + projection.subscribe((change) => { + delivered.push(change); + }); + + expect(() => { + commitResult = projection.commit( + cloneJSON(vector.operations), + commitOptions(vector.metadata), + ); + }).not.toThrow(); + expectRequired(commitResult, vector.expect.commit); + expectNotifications(delivered, vector.expect.delivered); + expect(projection.value).toEqual(vector.expect.value); +} + function runVector( harness: ProjectionHarness, vector: ProjectionVector, @@ -405,11 +552,20 @@ function runVector( case "unsubscribe": runUnsubscribeVector(harness, vector); return; + case "unsubscribe-during-publication": + runUnsubscribeDuringPublicationVector(harness, vector); + return; case "isolation": runIsolationVector(harness, vector); return; case "non-json": runNonJSONVector(harness, vector); + return; + case "reentrant-publication": + runReentrantPublicationVector(harness, vector); + return; + case "subscriber-error": + runSubscriberErrorVector(harness, vector); } } diff --git a/packages/json-document/tests/conformance/v2/projection-vectors.json b/packages/json-document/tests/conformance/v2/projection-vectors.json index 3d99217c..5c03656f 100644 --- a/packages/json-document/tests/conformance/v2/projection-vectors.json +++ b/packages/json-document/tests/conformance/v2/projection-vectors.json @@ -686,6 +686,72 @@ ] } }, + { + "id": "unsubscribe-during-publication-stops-current-delivery", + "kind": "unsubscribe-during-publication", + "requirements": [ + "JD2-COMMIT-001", + "JD2-PUBLISH-001" + ], + "operations": [ + { + "op": "replace", + "path": "/title", + "value": "Ready" + } + ], + "metadata": { + "origin": "intra-publication-unsubscribe" + }, + "expect": { + "commit": { + "ok": true, + "change": { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Ready" + } + ], + "metadata": { + "origin": "intra-publication-unsubscribe" + } + } + }, + "firstSubscriber": [ + { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Ready" + } + ], + "metadata": { + "origin": "intra-publication-unsubscribe" + } + } + ], + "secondSubscriber": [], + "value": { + "title": "Ready", + "items": [ + { + "id": "a", + "done": false + }, + { + "id": "b", + "done": false + } + ], + "meta": { + "owner": "core" + } + } + } + }, { "id": "references-are-isolated", "kind": "isolation", @@ -860,6 +926,205 @@ }, "notifications": [] } + }, + { + "id": "reentrant-publication-preserves-causal-order", + "kind": "reentrant-publication", + "requirements": [ + "JD2-COMMIT-001", + "JD2-COMMIT-002", + "JD2-PUBLISH-001" + ], + "outer": { + "operations": [ + { + "op": "replace", + "path": "/title", + "value": "Outer" + } + ], + "metadata": { + "origin": "outer" + } + }, + "nested": { + "operations": [ + { + "op": "replace", + "path": "/title", + "value": "Nested" + } + ], + "metadata": { + "origin": "nested" + } + }, + "expect": { + "outer": { + "ok": true, + "change": { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Outer" + } + ], + "metadata": { + "origin": "outer" + } + } + }, + "nested": { + "ok": true, + "change": { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Nested" + } + ], + "metadata": { + "origin": "nested" + } + } + }, + "firstSubscriber": [ + { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Outer" + } + ], + "metadata": { + "origin": "outer" + } + }, + { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Nested" + } + ], + "metadata": { + "origin": "nested" + } + } + ], + "secondSubscriber": [ + { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Outer" + } + ], + "metadata": { + "origin": "outer" + } + }, + { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Nested" + } + ], + "metadata": { + "origin": "nested" + } + } + ], + "value": { + "title": "Nested", + "items": [ + { + "id": "a", + "done": false + }, + { + "id": "b", + "done": false + } + ], + "meta": { + "owner": "core" + } + } + } + }, + { + "id": "subscriber-errors-do-not-escape-commit", + "kind": "subscriber-error", + "requirements": [ + "JD2-COMMIT-001", + "JD2-COMMIT-002", + "JD2-PUBLISH-001" + ], + "operations": [ + { + "op": "replace", + "path": "/title", + "value": "Delivered" + } + ], + "metadata": { + "origin": "subscriber-error" + }, + "expect": { + "commit": { + "ok": true, + "change": { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Delivered" + } + ], + "metadata": { + "origin": "subscriber-error" + } + } + }, + "delivered": [ + { + "applied": [ + { + "op": "replace", + "path": "/title", + "value": "Delivered" + } + ], + "metadata": { + "origin": "subscriber-error" + } + } + ], + "value": { + "title": "Delivered", + "items": [ + { + "id": "a", + "done": false + }, + { + "id": "b", + "done": false + } + ], + "meta": { + "owner": "core" + } + } + } } ], "nonRuntimeRequirements": [ diff --git a/packages/json-document/tests/conformance/v2/rfc6902-suite.ts b/packages/json-document/tests/conformance/v2/rfc6902-suite.ts new file mode 100644 index 00000000..24dc06af --- /dev/null +++ b/packages/json-document/tests/conformance/v2/rfc6902-suite.ts @@ -0,0 +1,80 @@ +import { describe, expect, test } from "vitest"; + +import specTests from "../../public/json-patch/conformance/spec_tests.json" with { type: "json" }; +import tests from "../../public/json-patch/conformance/tests.json" with { type: "json" }; +import type { + JSONPatchOperation, + ProtocolPatchResult, +} from "./protocol-suite.js"; + +interface RFC6902Case { + readonly comment?: string; + readonly doc: unknown; + readonly patch: ReadonlyArray; + readonly expected?: unknown; + readonly error?: string; + readonly disabled?: boolean; +} + +export interface RFC6902Harness { + applyPatch( + value: unknown, + operations: ReadonlyArray, + ): ProtocolPatchResult; +} + +const allCases = [ + ...(tests as unknown as ReadonlyArray), + ...(specTests as unknown as ReadonlyArray), +]; + +export function runRFC6902Conformance(harness: RFC6902Harness): void { + describe("json-document v2 RFC 6902 vendor black-box conformance", () => { + const disabled = allCases.filter((testCase) => testCase.disabled).length; + + for (const [index, testCase] of allCases.entries()) { + const label = `[${index}] ${testCase.comment ?? "(no comment)"}`; + if (testCase.disabled) { + test.skip(label, () => undefined); + continue; + } + + test(label, () => { + const result = harness.applyPatch(testCase.doc, testCase.patch); + if ("expected" in testCase) { + expect(result.ok).toBe(true); + if (result.ok) { + expect(sameJSON(result.value, testCase.expected)).toBe(true); + } + return; + } + if (testCase.error !== undefined) { + expect(result.ok).toBe(false); + return; + } + expect(result.ok).toBe(true); + }); + } + + test("vendored suite size is intentional", () => { + expect(allCases).toHaveLength(112); + expect(allCases.length - disabled).toBe(108); + expect(disabled).toBe(4); + }); + }); +} + +function sameJSON(left: unknown, right: unknown): boolean { + return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); +} + +function canonical(value: unknown): unknown { + if (value === null || typeof value !== "object") return value; + if (Array.isArray(value)) return value.map(canonical); + const object = value as Record; + const normalized: Record = {}; + for (const key of Object.keys(object).sort()) { + normalized[key] = canonical(object[key]); + } + return normalized; +} diff --git a/packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts b/packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts new file mode 100644 index 00000000..40690640 --- /dev/null +++ b/packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts @@ -0,0 +1,12 @@ +import { createJSONDocument } from "@interactive-os/json-document"; + +import { + runJSONPathConformance, + type JSONPathHarness, +} from "../conformance/v2/jsonpath-suite.js"; + +const referenceHarness: JSONPathHarness = { + create: createJSONDocument, +}; + +runJSONPathConformance(referenceHarness); diff --git a/packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts b/packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts new file mode 100644 index 00000000..a4d80d22 --- /dev/null +++ b/packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts @@ -0,0 +1,24 @@ +import { + appendSegment, + buildPointer, + parentPointer, + parsePointer, + trackPointer, + tryParsePointer, +} from "@interactive-os/json-document"; + +import { + runPointerConformance, + type PointerHarness, +} from "../conformance/v2/pointer-suite.js"; + +const referenceHarness: PointerHarness = { + appendSegment, + buildPointer, + parentPointer, + parsePointer, + trackPointer, + tryParsePointer, +}; + +runPointerConformance(referenceHarness); diff --git a/packages/json-document/tests/public/v2-projection-standard-conformance.test.ts b/packages/json-document/tests/public/v2-projection-standard-conformance.test.ts index 6da3cd1b..308a3ec0 100644 --- a/packages/json-document/tests/public/v2-projection-standard-conformance.test.ts +++ b/packages/json-document/tests/public/v2-projection-standard-conformance.test.ts @@ -82,7 +82,7 @@ test("nested commits preserve one causal publication order for every subscriber" expect(second).toEqual([1, 2]); }); -test("a throwing subscriber cannot prevent delivery to later subscribers", () => { +test("a throwing subscriber cannot escape commit or prevent later delivery", () => { const document = createJSONDocument({ value: 0 }); const delivered: JSONValue[] = []; @@ -95,9 +95,16 @@ test("a throwing subscriber cannot prevent delivery to later subscribers", () => : null); }); - expect(() => { - document.commit([{ op: "replace", path: "/value", value: 1 }]); - }).toThrow("subscriber failed"); + const result = document.commit([ + { op: "replace", path: "/value", value: 1 }, + ]); + + expect(result).toMatchObject({ + ok: true, + change: { + applied: [{ op: "replace", path: "/value", value: 1 }], + }, + }); expect(delivered).toEqual([1]); expect(document.value).toEqual({ value: 1 }); }); @@ -243,6 +250,42 @@ test("initial and acceptance boundaries fail without publishing invalid state", expect(observed).toEqual([]); }); +test("malformed acceptance results fail closed without changing state", () => { + expect(() => createJSONDocument( + { value: 0 }, + { accepts: () => undefined as never }, + )).toThrow(TypeError); + + const document = createJSONDocument( + { value: 0 }, + { + accepts(candidate) { + return isRecord(candidate) && candidate.value === 1 + ? undefined as never + : { ok: true }; + }, + }, + ); + const observed: unknown[] = []; + document.subscribe((change) => observed.push(change)); + const operations = [{ + op: "replace", + path: "/value", + value: 1, + }] as const; + + expect(document.canPatch(operations)).toMatchObject({ + ok: false, + code: "schema_violation", + }); + expect(document.commit(operations)).toMatchObject({ + ok: false, + code: "schema_violation", + }); + expect(document.value).toEqual({ value: 0 }); + expect(observed).toEqual([]); +}); + test("acceptance evaluation blocks reentrant probes and commits without changing state", () => { let document: ReturnType | undefined; const nestedResults: unknown[] = []; diff --git a/packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts b/packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts new file mode 100644 index 00000000..71afabb8 --- /dev/null +++ b/packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts @@ -0,0 +1,12 @@ +import { applyPatch } from "@interactive-os/json-document"; + +import { + runRFC6902Conformance, + type RFC6902Harness, +} from "../conformance/v2/rfc6902-suite.js"; + +const referenceHarness: RFC6902Harness = { + applyPatch, +}; + +runRFC6902Conformance(referenceHarness); diff --git a/scripts/evaluate-standardization.mjs b/scripts/evaluate-standardization.mjs index cc9398e6..c74f1ae1 100644 --- a/scripts/evaluate-standardization.mjs +++ b/scripts/evaluate-standardization.mjs @@ -92,6 +92,13 @@ function validateV2Conformance( protocolVectors, protocolSuiteSource, protocolReferenceSource, + pointerVectors, + pointerSuiteSource, + pointerReferenceSource, + rfc6902SuiteSource, + rfc6902ReferenceSource, + jsonPathSuiteSource, + jsonPathReferenceSource, requirementSet, ) { const expectedArtifacts = { @@ -101,6 +108,13 @@ function validateV2Conformance( protocolVectors: "packages/json-document/tests/conformance/v2/protocol-vectors.json", protocolSuite: "packages/json-document/tests/conformance/v2/protocol-suite.ts", protocolBinding: "packages/json-document/tests/public/v2-protocol-standard-conformance.test.ts", + pointerVectors: "packages/json-document/tests/conformance/v2/pointer-vectors.json", + pointerSuite: "packages/json-document/tests/conformance/v2/pointer-suite.ts", + pointerBinding: "packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts", + rfc6902Suite: "packages/json-document/tests/conformance/v2/rfc6902-suite.ts", + rfc6902Binding: "packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts", + jsonPathSuite: "packages/json-document/tests/conformance/v2/jsonpath-suite.ts", + jsonPathBinding: "packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts", }; if (JSON.stringify(draft.conformance) !== JSON.stringify(expectedArtifacts)) { fail("v2 conformance: artifact paths changed without updating the evaluator."); @@ -133,8 +147,11 @@ function validateV2Conformance( "read", "commit", "unsubscribe", + "unsubscribe-during-publication", "isolation", "non-json", + "reentrant-publication", + "subscriber-error", ]); if (!Array.isArray(projectionVectors.vectors)) { fail("v2 conformance vectors: vectors must be an array."); @@ -307,6 +324,83 @@ function validateV2Conformance( protocolReferenceSource, /applyPatch[\s\S]*runProtocolConformance/, ); + + if ( + pointerVectors.formatVersion !== 1 + || pointerVectors.status !== "candidate" + || pointerVectors.profile !== "docs/standard/v2-projection-profile.md" + || JSON.stringify(pointerVectors.requirements) + !== JSON.stringify(["JD2-ADDRESS-001", "JD2-PATCH-001"]) + ) { + fail("v2 Pointer vectors: metadata drifted."); + } + for (const group of ["parse", "invalid", "build", "append", "parent", "track"]) { + if (!Array.isArray(pointerVectors[group]) || pointerVectors[group].length === 0) { + fail(`v2 Pointer vectors: ${group} must be a non-empty array.`); + } + } + if ( + /@interactive-os\/json-document|\/src\//.test(pointerSuiteSource) + || !/PointerHarness[\s\S]*runPointerConformance/.test(pointerSuiteSource) + ) { + fail("v2 Pointer suite must be an implementation-independent injected harness."); + } + if ( + !/from "@interactive-os\/json-document"/.test(pointerReferenceSource) + || /\/src\/|domain\/|foundation\//.test(pointerReferenceSource) + ) { + fail("v2 Pointer binding must import only the public root package."); + } + for (const symbol of [ + "appendSegment", + "buildPointer", + "parentPointer", + "parsePointer", + "trackPointer", + "tryParsePointer", + ]) { + requirePattern( + `v2 Pointer binding ${symbol}`, + pointerReferenceSource, + new RegExp(`\\b${symbol}\\b`), + ); + } + + if ( + /@interactive-os\/json-document|\/src\//.test(rfc6902SuiteSource) + || !/RFC6902Harness[\s\S]*runRFC6902Conformance/.test(rfc6902SuiteSource) + ) { + fail("v2 RFC 6902 suite must be an implementation-independent injected harness."); + } + if ( + !/from "@interactive-os\/json-document"/.test(rfc6902ReferenceSource) + || /\/src\/|domain\/|foundation\//.test(rfc6902ReferenceSource) + ) { + fail("v2 RFC 6902 binding must import only the public root package."); + } + requirePattern( + "v2 RFC 6902 binding", + rfc6902ReferenceSource, + /applyPatch[\s\S]*runRFC6902Conformance/, + ); + + if ( + /@interactive-os\/json-document|\/src\//.test(jsonPathSuiteSource) + || !/JSONPathHarness[\s\S]*runJSONPathConformance/.test(jsonPathSuiteSource) + ) { + fail("v2 RFC 9535 suite must be an implementation-independent injected harness."); + } + if ( + !/from "@interactive-os\/json-document"/.test(jsonPathReferenceSource) + || /\/src\/|domain\/|foundation\//.test(jsonPathReferenceSource) + ) { + fail("v2 RFC 9535 binding must import only the public root package."); + } + requirePattern( + "v2 RFC 9535 binding", + jsonPathReferenceSource, + /createJSONDocument[\s\S]*runJSONPathConformance/, + ); } function validateV2PublicSurface( @@ -321,6 +415,13 @@ function validateV2PublicSurface( protocolVectors, protocolSuiteSource, protocolReferenceSource, + pointerVectors, + pointerSuiteSource, + pointerReferenceSource, + rfc6902SuiteSource, + rfc6902ReferenceSource, + jsonPathSuiteSource, + jsonPathReferenceSource, packageVersion, ) { if (draft.formatVersion !== 1) { @@ -329,8 +430,16 @@ function validateV2PublicSurface( if (draft.status !== "candidate") { fail("v2 public surface: status must be candidate before the stability gate passes."); } - if (draft.sourceContract !== "packages/json-document/public-contract.json#session") { - fail("v2 public surface: sourceContract must point to the preserved 1.x Session baseline."); + if (draft.sourceContract !== "packages/json-document/public-contract.json#root") { + fail("v2 public surface: sourceContract must point to the v2 root contract."); + } + if ( + draft.migrationBaselineContract + !== "packages/json-document/public-contract.json#session" + ) { + fail( + "v2 public surface: migrationBaselineContract must point to the 1.x Session baseline.", + ); } const requirementIds = Array.isArray(draft.requirements) ? draft.requirements : []; @@ -634,6 +743,13 @@ function validateV2PublicSurface( protocolVectors, protocolSuiteSource, protocolReferenceSource, + pointerVectors, + pointerSuiteSource, + pointerReferenceSource, + rfc6902SuiteSource, + rfc6902ReferenceSource, + jsonPathSuiteSource, + jsonPathReferenceSource, requirementSet, ); } @@ -656,6 +772,15 @@ const v2ProtocolVectors = JSON.parse( ); const v2ProtocolSuite = read(v2PublicSurface.conformance.protocolSuite); const v2ProtocolReference = read(v2PublicSurface.conformance.protocolBinding); +const v2PointerVectors = JSON.parse( + read(v2PublicSurface.conformance.pointerVectors), +); +const v2PointerSuite = read(v2PublicSurface.conformance.pointerSuite); +const v2PointerReference = read(v2PublicSurface.conformance.pointerBinding); +const v2RFC6902Suite = read(v2PublicSurface.conformance.rfc6902Suite); +const v2RFC6902Reference = read(v2PublicSurface.conformance.rfc6902Binding); +const v2JSONPathSuite = read(v2PublicSurface.conformance.jsonPathSuite); +const v2JSONPathReference = read(v2PublicSurface.conformance.jsonPathBinding); const v2SignatureContract = read( "packages/json-document/tests/public/v2-signature-contract.test-d.ts", ); @@ -845,6 +970,13 @@ validateV2PublicSurface( v2ProtocolVectors, v2ProtocolSuite, v2ProtocolReference, + v2PointerVectors, + v2PointerSuite, + v2PointerReference, + v2RFC6902Suite, + v2RFC6902Reference, + v2JSONPathSuite, + v2JSONPathReference, libraryPackage.version, ); From 735e9e30d0bf8a7786c5d1fb932910505d02c622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Sun, 26 Jul 2026 11:52:04 +0900 Subject: [PATCH 2/4] refactor: ship the v2 kernel only --- .github/workflows/publish.yml | 3 + README.md | 12 +- apps/site/src/routes/Home.tsx | 5 +- apps/site/src/site-routes.json | 2 +- docs/README.md | 17 +- docs/changelog.md | 12 +- docs/public/api.md | 82 +- docs/public/extensions.md | 8 +- docs/public/overview.md | 23 +- docs/public/quickstart.md | 60 +- docs/public/recipes.md | 4 +- docs/standard/v2-projection-profile.md | 104 +- docs/standard/v2-public-surface.json | 406 +--- llms.txt | 42 +- package-lock.json | 25 - package.json | 6 +- packages/json-document/README.md | 80 +- packages/json-document/package.json | 20 - packages/json-document/public-contract.json | 150 -- .../src/foundation/json/clone.ts | 2 +- .../src/foundation/json/serializable.ts | 2 +- .../src/foundation/jsonpath/ast.ts | 2 +- .../src/foundation/jsonpath/index.ts | 2 +- .../src/foundation/patch/apply.ts | 2 +- .../src/foundation/patch/container.ts | 2 +- .../src/foundation/patch/contract.ts | 9 +- .../src/foundation/patch/fast/array.ts | 2 +- .../src/foundation/patch/fast/replace.ts | 2 +- .../src/foundation/patch/index.ts | 3 +- .../src/foundation/patch/inverse.ts | 2 +- .../src/foundation/patch/path.ts | 2 +- .../src/foundation/patch/result.ts | 5 +- .../src/foundation/patch/schema-contract.ts | 11 + .../src/foundation/patch/schema.ts | 7 +- .../src/foundation/patch/source.ts | 2 +- .../src/foundation/patch/track.ts | 2 +- .../src/foundation/patch/value.ts | 2 +- .../src/foundation/pointer/array.ts | 2 +- .../src/foundation/pointer/core.ts | 179 ++ .../src/foundation/pointer/index.ts | 198 +- .../src/foundation/pointer/siblingRange.ts | 2 +- .../src/foundation/pointer/source.ts | 2 +- .../src/foundation/protocol/contract.ts | 2 +- .../src/foundation/protocol/index.ts | 6 +- .../tests/public/package-exports.test.ts | 8 +- .../tests/smoke/package-smoke.mjs | 1104 ++--------- packages/json-document/tsconfig.json | 2 +- scripts/benchmark-core.mjs | 1683 +---------------- scripts/evaluate-docs.mjs | 43 +- scripts/evaluate-standardization.mjs | 1340 +++++-------- 50 files changed, 1124 insertions(+), 4569 deletions(-) create mode 100644 packages/json-document/src/foundation/patch/schema-contract.ts create mode 100644 packages/json-document/src/foundation/pointer/core.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 240e00db..574ec916 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -118,6 +118,9 @@ jobs: npm run verify -w "$package" npm pack -w "$package" --dry-run --cache "${RUNNER_TEMP}/npm-cache" done + - name: Verify core prerelease + if: startsWith(github.ref_name, 'json-document-v') + run: npm run release:check - name: Publish core prerelease if: startsWith(github.ref_name, 'json-document-v') run: npm publish -w @interactive-os/json-document --tag next --provenance diff --git a/README.md b/README.md index 9d6531b2..a74e8e98 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ v2 root는 JSON, JSON Pointer, JSONPath, JSON Patch만 전제로 하며 Zod, Rea selection, clipboard, history를 필수 계약에 넣지 않습니다. ```txt -Pure Protocol - |-> Document Projection -> host adapter - `-> Candidate Editing Session -> React / rich host adapter +Pure Protocol -> Document Projection -> host adapter ``` 공식 사이트: https://developer-1px.github.io/json-document/ @@ -27,13 +25,13 @@ Pure Protocol | 변경 기록 | [docs/changelog.md](docs/changelog.md) | | v2 Projection 표준 | [docs/standard/v2-projection-profile.md](docs/standard/v2-projection-profile.md) | | v2 공개 표면 manifest | [docs/standard/v2-public-surface.json](docs/standard/v2-public-surface.json) | -| Candidate Session의 1.x 기준선 | [docs/standard/conformance-profile.md](docs/standard/conformance-profile.md) | +| archived 1.x 기준선 | [docs/standard/conformance-profile.md](docs/standard/conformance-profile.md) | ## 코드 지도 | 위치 | 역할 | | --- | --- | -| [packages/json-document](packages/json-document) | v2 Kernel과 optional Candidate Session | +| [packages/json-document](packages/json-document) | v2 Kernel과 archived 1.x source | | [packages/collection](packages/collection) | ordered JSON array item 이동/복제/삭제 | | [packages/clipboard-web](packages/clipboard-web) | browser clipboard bridge | | [packages/contenteditable-web](packages/contenteditable-web) | `@interactive-os/json-document-contenteditable-web` DOM contenteditable text-surface adapter | @@ -68,12 +66,10 @@ v2 Kernel이 제공하는 최소 계약: - ordered atomic JSON Patch commit - canonical applied change publication -`@interactive-os/json-document/session`은 Zod 기반 schema 검증, selection, -clipboard, history와 고수준 편집 동사를 제공하는 optional Candidate 표면입니다. - 편집 툴이 계속 소유하는 것: - rendering, DOM focus, keyboard, drag/drop UI +- selection, clipboard, history, framework lifecycle - grid selection, TSV clipboard, formula engine - product command 이름, layout, route, remote protocol diff --git a/apps/site/src/routes/Home.tsx b/apps/site/src/routes/Home.tsx index 68f24c25..454a7101 100644 --- a/apps/site/src/routes/Home.tsx +++ b/apps/site/src/routes/Home.tsx @@ -2,8 +2,7 @@ const modelRows = [ ["protocol", "provider-neutral JSON Patch application"], ["projection", "value, at, query, canPatch, commit, subscribe"], ["acceptance", "optional provider-neutral candidate validation"], - ["session", "optional selection, clipboard, history, rich edit verbs"], - ["host", "rendering, focus, keyboard, persistence, collaboration"], + ["host", "selection, history, rendering, focus, persistence, collaboration"], ] as const; const BASE_PATH = import.meta.env.BASE_URL.replace(/\/$/, ""); @@ -83,7 +82,7 @@ export function Home() {

Boundary

  • Root Kernel is React- and Zod-free.
  • -
  • Rich editing lives under the optional @interactive-os/json-document/session boundary.
  • +
  • Rich editing belongs to host adapters composed over the six-member document.
  • Mutation inputs are JSON Patch with JSON Pointer paths.
  • JSONPath is search-only and returns pointers.
diff --git a/apps/site/src/site-routes.json b/apps/site/src/site-routes.json index 78c3c53f..88e4cad1 100644 --- a/apps/site/src/site-routes.json +++ b/apps/site/src/site-routes.json @@ -10,7 +10,7 @@ "path": "/docs", "label": "Docs", "title": "json-document Docs - json-document", - "description": "User guide to the v2 Kernel, Pure Protocol, Document Projection, optional Editing Session, and host boundaries.", + "description": "User guide to the v2 Kernel, Pure Protocol, Document Projection, and host adapter boundaries.", "group": "Start" }, { diff --git a/docs/README.md b/docs/README.md index 6fe57620..8534171f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,8 +17,8 @@ docs | |-- extensions.md # 공식 extension 사용법 | `-- recipes.md # 제품별 feature coverage 지도 `-- standard - |-- core-standard.md # 1.x Editing Session migration baseline - |-- conformance-profile.md # 1.x compatibility profile + |-- core-standard.md # archived 1.x baseline + |-- conformance-profile.md # archived 1.x profile |-- contract-pressure-register.md |-- extension-delegation-standard.md |-- foundation-gate.md # foundation 판정 기준 @@ -27,8 +27,8 @@ docs |-- selection-contract.md # headless selection 의미론 |-- self-improvement-loop-report.md |-- v2-projection-profile.md # 현재 v2 root 규범 profile - |-- v2-public-surface.json # v2 공개 binding과 migration disposition - `-- json-document-spec.md # 1.x Editing Session 의미론 baseline + |-- v2-public-surface.json # v2 공개 binding + `-- json-document-spec.md # archived 1.x 의미론 ``` ## 규범 우선순위 @@ -36,10 +36,9 @@ docs 현재 v2 portable root의 정본은 `v2-projection-profile.md`, `v2-public-surface.json`, 그리고 profile이 지정한 conformance vector와 language binding이다. `core-standard.md`, `conformance-profile.md`, -`json-document-spec.md`와 selection/schema/result 세부 계약은 1.x rich -runtime을 `/session` Candidate 경계로 옮기기 위한 migration baseline이다. -이전 문서의 root 또는 core 표현은 v2 exact 20-symbol·six-member 계약을 -확장하지 않으며, 충돌하면 v2 artifact가 우선한다. +`json-document-spec.md`와 selection/schema/result 세부 계약은 archived 1.x +기록이다. 이전 문서의 root 또는 core 표현은 v2 exact +20-symbol·six-member 계약을 확장하지 않으며, 충돌하면 v2 artifact가 우선한다. ## 책임 기준 @@ -48,7 +47,7 @@ runtime을 `/session` Candidate 경계로 옮기기 위한 migration baseline이 | `changelog.md` | 사용자 영향 중심 변경 기록 | 외부 사용자, 릴리스 확인자 | | `generated/` | repo scan으로 만든 reference data. 직접 편집하지 않는다. | 사이트, evaluator | | `public/` | 사용법과 프로젝트 이해를 위한 공식 문서 원천 | 외부 사용자, LLM, 사이트 방문자 | -| `standard/` | v2 root 정본과 1.x Session migration baseline | 표준화 검토자, 대체 구현 작성자 | +| `standard/` | v2 root 정본과 archived 1.x baseline | 표준화 검토자, 대체 구현 작성자 | ## 작성 원칙 diff --git a/docs/changelog.md b/docs/changelog.md index a5ce288f..a8c6d32a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,13 +7,11 @@ All notable changes to this project are documented here. - **Breaking (`2.0.0-rc.0`):** Replaced the package root with the provider-neutral v2 Kernel: exactly 8 runtime values and 12 public types, with a non-generic six-member `JSONDocument` projection. -- Moved the Zod-based 1.x rich editing runtime, including selection, clipboard, - history, schema introspection, and high-level edit verbs, to the Candidate - `@interactive-os/json-document/session` entrypoint. The React entrypoint - continues to adapt that Candidate Session. -- Marked the package's React and Zod peers optional so Root Protocol and - Projection consumers have no framework or schema-provider installation - requirement. +- Removed the 1.x `/session` and `/react` package entrypoints. Their source and + regression tests remain archived in the repository but are not compiled or + shipped by the production package. +- Removed React and Zod peer dependencies. Root Protocol and Projection + consumers install no runtime or peer dependencies. - Narrowed the id resolver, stable-id rebase, and causal inbox inputs to their actual read/preflight/publication ports. Full `JSONDocument` values remain compatible, while host adapters can guard a projection without exposing the diff --git a/docs/public/api.md b/docs/public/api.md index f5e0923c..fe0647f2 100644 --- a/docs/public/api.md +++ b/docs/public/api.md @@ -1,7 +1,6 @@ # json-document API -이 문서는 v2 루트 Kernel의 정본 API와 선택적인 Candidate Editing Session을 -분리해서 설명합니다. +이 문서는 v2 루트 Kernel의 정본 API를 설명합니다. ```txt @interactive-os/json-document @@ -9,12 +8,6 @@ |-- applyPatch |-- Pointer helpers `-- six-member JSONDocument - -@interactive-os/json-document/session -`-- Candidate rich editing runtime - -@interactive-os/json-document/react -`-- Candidate Editing Session React adapter ``` ## 기준 @@ -25,7 +18,6 @@ - JSONPath는 query 전용이며 결과를 Pointer 배열로 돌려줍니다. - `canPatch`는 side effect 없는 probe이고 `commit`만 state를 변경합니다. - 실패는 boolean `ok`와 stable string `code`를 가진 Result입니다. -- `/session`과 `/react`는 optional Candidate이며 Core 적합성에 필요하지 않습니다. ## 작업별 진입점 @@ -41,8 +33,8 @@ | Pointer 조합 | `buildPointer`, `appendSegment`, `parentPointer` | `Pointer` | | 변경 뒤 Pointer 추적 | `trackPointer` | `Pointer | null` | -Selection, clipboard, history와 high-level edit verb는 Candidate Editing Session -항목을 봅니다. +Selection, clipboard, history와 high-level edit verb는 host 또는 adapter가 +`JSONDocument` 위에서 소유합니다. ## 시작 @@ -314,68 +306,8 @@ types ReadResult, QueryResult, JSONDocument ``` -## Candidate Editing Session - -`@interactive-os/json-document/session`은 Zod 기반 rich editing runtime을 보존한 -선택 표면입니다. 현재 Candidate이므로 portable Core consumer는 이 subpath나 -세부 signature에 의존하지 않습니다. - -```ts -import * as z from "zod"; -import { - createJSONDocument as createJSONEditingSession, -} from "@interactive-os/json-document/session"; - -const Schema = z.object({ - title: z.string(), - cards: z.array(z.object({ id: z.string() })), -}); - -const session = createJSONEditingSession( - Schema, - { title: "Draft", cards: [] }, - { history: 100, selection: true }, -); -``` - -Candidate Session에는 다음과 같은 기능이 있습니다. - -```txt -patch, insert, replace, delete, move, duplicate -find, canFind, entries -selection, copy, cut, paste -history, undo, redo -schema introspection -``` - -이 이름은 root `JSONDocument`의 필수 member가 아닙니다. Session의 -`violations[].path`는 JSON Pointer이며 schema 검사의 기준은 `schema-slot`, -mutation preflight의 기준은 `document-result`입니다. - -Session execution의 기본값은 `strict: false`입니다. `strict: true`를 선택한 -Session에서 처리된 실행 실패는 error로 승격될 수 있지만 Core `commit`의 -expected input failure는 Result입니다. - -Session 최적화 문맥에서 내부 snapshot을 신뢰된 document state라고 부릅니다. -구조만 가진 Zod schema는 좁은 validation 경로를 사용할 수 있고, refine, -transform 또는 custom check가 있으면 전체 루트 schema 검증으로 돌아갑니다. -이 최적화 어휘와 provider detail은 v2 Core 계약이 아닙니다. - -## Candidate React adapter - -현재 `@interactive-os/json-document/react`의 `useJSONDocument`는 Candidate -Editing Session을 React lifecycle에 연결합니다. - -```tsx -import { useJSONDocument } from "@interactive-os/json-document/react"; - -const session = useJSONDocument( - Schema, - { title: "Draft", cards: [] }, - { history: 20 }, -); -``` +## Host와 adapter -Provider-neutral six-member Projection만 필요한 React app은 Core document를 -별도 lifecycle wrapper에 보관할 수 있습니다. 현재 hook을 portable Core -adapter로 가정하지 않습니다. +패키지는 `/session`이나 `/react` subpath를 공개하지 않습니다. Selection, +clipboard, history, schema introspection, DOM lifecycle과 framework binding은 +host 또는 별도 adapter가 여섯-member `JSONDocument`를 조합해 구현합니다. diff --git a/docs/public/extensions.md b/docs/public/extensions.md index 1b717b52..5a2d2cdc 100644 --- a/docs/public/extensions.md +++ b/docs/public/extensions.md @@ -6,9 +6,7 @@ lookup 같은 앱별 책임은 extension이나 host code에서 조립합니다. Extension은 core에 plugin으로 등록하지 않습니다. Portable extension은 public `JSONDocument` 여섯 member를 받아 함수로 compose합니다. Selection, clipboard, -history 또는 high-level edit verb를 요구하는 기존 rich extension은 Candidate -`@interactive-os/json-document/session`에 의존할 수 있으므로 package별 입력 -contract를 확인합니다. +history 또는 high-level edit verb는 extension이나 host가 소유합니다. ## 설치 @@ -102,7 +100,9 @@ const persistence = createDocumentPersistence(doc, { key: "article-draft" }); editorView.dispatch(editorTransaction); doc.commit([ { op: "replace", path: "/doc", value: prosemirrorToJson(editorView.state.doc) }, -], { label: "edit rich text", origin: "prosemirror" }); +], { + metadata: { label: "edit rich text", origin: "prosemirror" }, +}); await persistence.save(); ``` diff --git a/docs/public/overview.md b/docs/public/overview.md index 75cbcce1..1b867b72 100644 --- a/docs/public/overview.md +++ b/docs/public/overview.md @@ -6,9 +6,7 @@ json-document v2는 문서, 표, 슬라이드, 캔버스, 노트 편집기가 React, selection, clipboard, history를 필수 계약에 넣지 않습니다. ```txt -Pure Protocol - |-> Document Projection -> host adapter - `-> Candidate Editing Session -> React / rich host adapter +Pure Protocol -> Document Projection -> host adapter ``` ## 배경 @@ -40,7 +38,7 @@ CRDT와 OT는 host 또는 extension 책임입니다. | Pure Protocol | 현재 document instance 없이 JSON Patch를 적용하는 함수 | | Document Projection | 현재 snapshot에 read, probe, commit, publication을 연결한 여섯-member port | | acceptance | candidate state를 publish 전에 검사하는 provider-neutral callback | -| Editing Session | selection, clipboard, history와 고수준 편집 동사를 가진 Candidate 선택 표면 | +| Host adapter | selection, clipboard, history, DOM과 고수준 편집 동사를 소유하는 별도 계층 | 가장 중요한 경계는 query와 mutation을 섞지 않는 것입니다. @@ -105,17 +103,15 @@ if (result.ok) { } ``` -## Core와 Editing Session +## Core와 host | 표면 | 상태 | 책임 | | --- | --- | --- | | `@interactive-os/json-document` | v2 Kernel | Pure Protocol과 여섯-member Projection | -| `@interactive-os/json-document/session` | Candidate | Zod 기반 rich editing, selection, clipboard, history | -| `@interactive-os/json-document/react` | Candidate | 현재 Editing Session을 React lifecycle에 연결 | -`/session`과 `/react`는 v2 portable Core 적합성에 필요하지 않은 선택 표면입니다. -Candidate 기간에는 이름과 세부 signature가 바뀔 수 있으므로, 구현 간 교환 가능한 -코드는 루트 `JSONDocument` 여섯 member에만 의존합니다. +패키지는 `/session`과 `/react`를 공개하지 않습니다. 구현 간 교환 가능한 코드는 +루트 `JSONDocument` 여섯 member에만 의존하고, 편집 UX와 framework lifecycle은 +host 또는 별도 adapter가 소유합니다. ## 자주 쓰는 작업 @@ -128,7 +124,7 @@ Candidate 기간에는 이름과 세부 signature가 바뀔 수 있으므로, | 변경 구독 | `document.subscribe(listener)` | | instance 없는 patch 적용 | `applyPatch(value, operations)` | | Pointer 조합과 추적 | `buildPointer`, `appendSegment`, `parentPointer`, `trackPointer` | -| selection, clipboard, undo/redo | Candidate Editing Session | +| selection, clipboard, undo/redo | host 또는 별도 adapter | 성공한 mutation의 `change.applied`는 실제 적용된 canonical operation입니다. 실패는 throw 대신 `{ ok: false, code, reason?, pointer? }` result로 표현됩니다. @@ -141,6 +137,5 @@ Candidate 기간에는 이름과 세부 signature가 바뀔 수 있으므로, - Slide와 whiteboard: object property와 layer state를 headless JSON으로 관리 - 저장과 협업 adapter: subscribed canonical change를 외부 log로 전달 -제품별 selection, clipboard, history가 필요하면 Candidate Session 또는 작은 -extension을 조합합니다. Core Projection은 그 기능을 필수 member로 요구하지 -않습니다. +제품별 selection, clipboard, history는 host 또는 작은 extension에서 +조합합니다. Core Projection은 그 기능을 필수 member로 요구하지 않습니다. diff --git a/docs/public/quickstart.md b/docs/public/quickstart.md index e9d989aa..65233bd6 100644 --- a/docs/public/quickstart.md +++ b/docs/public/quickstart.md @@ -169,61 +169,5 @@ const acceptedDocument = createJSONDocument(initialBoard, { Acceptance는 candidate를 허용하거나 거부할 뿐 commit-time transform을 몰래 state에 적용하지 않습니다. -## 7. Candidate Editing Session - -Selection, clipboard, history와 `insert`, `replace`, `delete`, `move`, -`duplicate`, `copy`, `cut`, `paste`, `undo`, `redo`가 필요하면 선택적인 -`/session` binding을 사용할 수 있습니다. - -```ts -import * as z from "zod"; -import { - createJSONDocument as createJSONEditingSession, -} from "@interactive-os/json-document/session"; - -const Board = z.object({ - lists: z.array(z.object({ - id: z.string(), - title: z.string(), - cards: z.array(z.object({ - id: z.string(), - title: z.string(), - status: z.enum(["todo", "doing", "done"]), - })), - })), -}); - -const session = createJSONEditingSession( - Board, - initialBoard, - { history: 100, selection: true }, -); - -session.insert("/lists/0/cards/-", { - id: "c2", - title: "Review API", - status: "todo", -}); -session.undo(); -``` - -위 `createJSONEditingSession`은 현재 동명 export를 구분하기 위한 local import -alias입니다. `/session`은 Candidate이며 portable v2 Core 계약이 아닙니다. -Pointer 배열을 copy하면 clipboard payload도 배열이라는 규칙 역시 이 Session -계층의 계약입니다. - -## 8. React에서 쓰기 - -현재 `/react`의 `useJSONDocument`는 Candidate Editing Session adapter입니다. - -```tsx -import { useJSONDocument } from "@interactive-os/json-document/react"; - -const session = useJSONDocument(Board, initialBoard, { - history: 100, - selection: true, -}); -``` - -React 없이 Core를 쓰는 코드는 루트 `createJSONDocument`를 사용합니다. `/react` -hook을 여섯-member provider-neutral Projection adapter로 가정하지 않습니다. +Selection, clipboard, history, DOM lifecycle과 React binding은 이 패키지의 +subpath가 아닙니다. 별도 host 또는 adapter가 위 `document`를 조합합니다. diff --git a/docs/public/recipes.md b/docs/public/recipes.md index 5cc86668..117e6493 100644 --- a/docs/public/recipes.md +++ b/docs/public/recipes.md @@ -18,7 +18,7 @@ product feature v2 Core는 JSON value, Pointer, Patch, query, `canPatch`, `commit`과 publication을 맡습니다. Schema provider는 acceptance adapter로 연결합니다. Selection, -clipboard, history와 high-level edit verb는 Candidate Editing Session입니다. +clipboard, history와 high-level edit verb는 host 또는 extension 책임입니다. Rendering, focus, keyboard, parser, auth, remote sync는 앱 책임입니다. ## Kanban @@ -28,7 +28,7 @@ Rendering, focus, keyboard, parser, auth, remote sync는 앱 책임입니다. | Board/card state | Core `createJSONDocument` + acceptance | schema 설계 | | Card field edit | Core `canPatch`, `commit` | field UI | | Card/list reorder | `@interactive-os/json-document-collection` | drag target 계산 | -| Duplicate card | Candidate Session `duplicate` 또는 explicit Patch | id 정책 | +| Duplicate card | explicit Patch 또는 collection extension | id 정책 | | Browser clipboard | `@interactive-os/json-document-clipboard-web` | native shortcut UI | | Card form | `@interactive-os/json-document-schema-form` | widget rendering | | Dirty/save | `@interactive-os/json-document-dirty-state`, `@interactive-os/json-document-persist-web` | server sync | diff --git a/docs/standard/v2-projection-profile.md b/docs/standard/v2-projection-profile.md index 216c5279..aaa3262e 100644 --- a/docs/standard/v2-projection-profile.md +++ b/docs/standard/v2-projection-profile.md @@ -15,14 +15,11 @@ reference implementation의 현재 동작은 상위 계약을 덮어쓸 수 없 Pure Protocol은 JSON value, JSON Pointer, JSONPath, JSON Patch와 result의 의미만 정의한다. Projection은 그 protocol을 현재 문서에 연결하는 stateful -port다. Host adapter는 Projection과 조합한다. Candidate Editing Session은 -같은 protocol을 쓰는 별도 선택 표면이며 Projection 대입 가능성을 아직 약속하지 -않는다. +port다. Selection, history, clipboard, DOM과 framework lifecycle은 Host +adapter가 Projection과 조합한다. ```txt -Pure Protocol - |-> Projection -> host adapter - `-> Candidate Editing Session -> React / rich host adapter +Pure Protocol -> Projection -> host adapter ``` ## 여섯 member @@ -77,11 +74,11 @@ JSON이 아니거나 acceptance에 거부되면 TypeScript reference binding은 | JD2-RESULT-002 | consumer는 모르는 result field와 error code를 일반적으로 처리할 수 있어야 한다. minor release는 기존 required field나 code의 의미를 바꾸면 안 되지만 optional field와 새 code는 MAY 추가할 수 있으므로 exact `Object.keys` 집합은 계약이 아니다. | | JD2-SCHEMA-001 | Projection이 acceptance rule을 구성하면 initial state와 commit candidate를 publish 전에 검사해야 한다. 이 boundary는 provider-neutral이어야 하며 `_zod`, provider issue, private schema object를 Projection 적합성에 요구하면 안 된다. 제약 없는 acceptance는 MAY 허용한다. Acceptance callback은 같은 Projection의 `canPatch`나 `commit`을 재진입해서는 안 되며, binding은 그런 호출을 failure로 차단해 state와 publication을 보존해야 한다. | | JD2-SCHEMA-002 | import나 initial parse는 commit 전에 명시적으로 값을 변환할 수 있다. commit-time acceptance는 candidate를 몰래 변환하면 안 되며 normalization이 필요하면 최종 value를 만드는 operation이 applied change에 명시되어야 한다. | -| JD2-SESSION-001 | insert, replace, delete, move, duplicate, selection, clipboard, history, schema introspection과 그 capability probe는 optional Editing Session profile이다. 이름을 유지할 수 있지만 Projection 적합성에는 요구하면 안 된다. | +| JD2-SESSION-001 | insert, replace, delete, move, duplicate, selection, clipboard, history, schema introspection과 그 capability probe는 host 또는 별도 adapter 책임이다. Kernel package의 export나 Projection 적합성에 요구하면 안 된다. | | JD2-HOST-001 | rendering, DOM focus, geometry, keyboard policy, system clipboard, filesystem, network, formula engine, CRDT와 OT는 host 또는 extension이 소유해야 하며 Core Projection의 필수 data나 member가 되어서는 안 된다. | | JD2-CONFORMANCE-001 | conformance는 public factory 또는 injected harness만 사용하는 machine-readable black-box vector로 성공, 실패, atomicity, immutability, probe/commit parity, publication을 검증해야 한다. private source path, provider object, 특정 dist layout을 요구하면 안 된다. | | JD2-CONFORMANCE-002 | 이 profile을 stable이라고 선언하려면 같은 suite가 reference implementation과 최소 한 개의 독립 구현을 통과하고 form, table/data-grid, outliner/tree, rich text, storage/collaboration의 다섯 pressure vertical에서 같은 제약이 확인되어야 한다. | -| JD2-BINDING-001 | package export, TypeScript overload, React hook은 언어별 binding contract이며 보편 protocol과 별도로 versioning해야 한다. public Projection declaration은 application-owned structural contract여야 하고 implementation runtime alias나 private declaration path를 노출하면 안 된다. | +| JD2-BINDING-001 | package export와 TypeScript declaration은 언어별 binding contract이며 보편 protocol과 별도로 versioning해야 한다. v2 package는 root entrypoint와 20개 Kernel symbol만 공개하고 runtime·peer dependency 없이 빌드되어야 한다. public Projection declaration은 application-owned structural contract여야 하고 archived session, framework binding, implementation runtime alias나 private declaration path를 노출하면 안 된다. | ## Result 초안 @@ -136,8 +133,8 @@ TypeScript binding은 non-JSON input도 expected failure result로 돌려주기 받으며 runtime에서 RFC 8259 boundary를 검사한다. 임의 JSON Patch는 root value의 shape까지 바꿀 수 있으므로 Projection snapshot은 거짓 generic을 노출하지 않고 항상 `JSONValue`다. 타입이 보장된 domain model은 acceptance를 -소유하는 Editing Session의 책임이다. `JSONChangeMetadata`의 selection과 history 전용 field는 Session -metadata로 분리하고, core metadata는 JSON object boundary만 정한다. Pure +소유하는 host adapter의 책임이다. `JSONChangeMetadata`의 selection과 history +전용 field는 host metadata로 분리하고, core metadata는 JSON object boundary만 정한다. Pure `applyPatch`는 새 value를 반환하지만 stateful `commit`은 `document.value`가 있으므로 snapshot을 중복 반환하지 않는다. unknown field와 unknown code를 허용하는 forward-compatibility fixture를 포함한다. @@ -171,74 +168,19 @@ schema-free `applyPatch`는 구현됐다. transform identity의 two-provider 검 독립 구현과 다섯 pressure vertical 통과는 아직 deferred이므로 Candidate를 stable이라고 선언할 수 없다. -## 공개 심볼 disposition - -아래 disposition은 1.x surface를 보존한 `/session` baseline의 migration -기록이다. `Kernel`은 v2 root 정본에 남긴 이름, `Session`은 optional profile, -`Compat`은 migration 이름, `Remove`는 root 정본에서 제거한 이름이다. -`Remove`는 기능 삭제를 뜻하지 -않으며 adapter, extension, provider package 또는 internal helper로 이동할 수 -있다. 이 disposition은 이름의 행선지이며 1.x signature 보존 여부가 아니다. -`Kernel` 이름도 v2 의미에 맞추는 major-version signature 변경은 허용한다. - -| Disposition | 현재 심볼 | 판단 | -| --- | ---: | --- | -| Kernel | 16 | protocol primitive와 여섯-member reference binding | -| Session | 20 | 검증된 편집 동사, controller, React와 sibling-range helper | -| Compat | 14 | 중복 이름, legacy alias, provider가 샌 schema boundary | -| Remove | 89 | 파생 alias, component result, 고급 selection, schema introspection, TextSurface | - -139개 이름의 유일한 분류와 replacement는 -[`v2-public-surface.json`](./v2-public-surface.json)이 정본이다. 이 manifest의 -`sourceContract`는 현재 v2 root를, `migrationBaselineContract`는 disposition의 -출처인 1.x `/session`을 각각 가리킨다. - -도입 이름은 `JSONValue`, `JSONAppliedChange`, `JSONPatchResult`, -`JSONDocumentCommitResult`, `JSONDocumentPlacementTarget`, -`SelectionSnapshot` 여섯 개다. 앞의 네 Kernel 이름은 root에 구현됐고, 뒤의 -두 Session 이름은 Session 안정화 전까지 미구현으로 남는다. 별도 `Projection` -public type은 외부 구현에서 같은 structural port가 반복되기 전에는 추가하지 -않는다. - -## family별 이동 - -| Family | 왜 생겼나 | v2 위치 | -| --- | --- | --- | -| JSON, Pointer, Patch, query, result | 주소·변경 의미의 구현 간 일치 | Kernel | -| derived Pointer helper와 exception | escaping, parent/index 계산의 실수 방지 | canonical Pointer 조합으로 대체하거나 Compat | -| familiar edit verb와 `can*` | 제품 코드가 patch plan을 반복하지 않게 함 | optional Editing Session | -| selection·clipboard·history controller | 30년간 수렴한 사용자 편집 상태와 동사 | optional Editing Session | -| component Ok/Error와 option alias | union narrowing과 method parameter 재사용 | canonical Result에서 `Extract`하거나 parameter에 inline | -| `entries`와 entry alias | outline·schema form의 traversal 편의 | `at` 위의 consumer traversal | -| schema acceptance | 잘못된 document publication 방지 | provider-neutral Projection construction boundary | -| schema introspection | schema-driven form과 insertion UI 생성 | provider adapter 또는 extension | -| advanced selection ordering·span·text edit | caret 이동, 범위 정렬, 문자열 편집 알고리즘 공유 | selection helper package | -| sibling-range 세부 alias | 여러 선택을 공통 parent/index run으로 정규화 | helper와 최종 Result만 Session에 유지하고 세부 type은 파생 | -| TextSurface | contenteditable 문자열과 atom/range sidecar 동기화 | rich-text adapter | -| React hook | component lifecycle과 document 연결 | React binding | -| trusted-state fast path | 검증·clone 생략 성능 최적화 | internal optimization | -| DOM·geometry·formula·persistence·collaboration | host와 제품마다 다른 실행 환경 | host 또는 extension | - -## 1.x migration - -| 1.x | v2 방향 | -| --- | --- | -| 39-member `JSONDocument` type | 여섯 member만 필수인 structural contract | -| `subscribe(applied, metadata)` | `subscribe(change)` | -| flat `JSONChangeMetadata` | JSON object boundary; selection/history key는 Session 소유 | -| `JSONDocumentCommitOptions.selectionAfter` | core metadata와 Session final-selection option 분리 | -| `JSONPatchOperation.value: unknown` | `JSONValue` | -| `patch` | `commit` wrapper로 한 migration window 유지 | -| `lastPatch` | commit result의 `change` 또는 subscribed change | -| `find`, `canFind`, `canQuery` | `query` result | -| `applyOperation` | one-element batch를 받는 `applyPatch` | -| `applyPatch(schema, value, operations)` | `applyPatch(value, operations)` 뒤 Session acceptance와 조합 | -| `JSONPatchInput` | `ReadonlyArray` | -| `JSONResult` | context에 따라 `JSONPatchResult` 또는 `JSONDocumentCommitResult` | -| insert/move target alias | `JSONDocumentPlacementTarget` | -| exact result key fixture | required field + unknown-field 허용 fixture | -| Zod-shaped schema boundary | provider-neutral acceptance adapter | - -v2 major 변경은 여섯 member black-box suite와 pure Protocol suite를 먼저 -고정한 뒤 root를 20개 Kernel symbol로 교체했다. 풍부한 1.x runtime은 -`/session` 변경 경계로 이동해 기능을 보존하되 portable Core 계약과 분리했다. +## Package binding + +`@interactive-os/json-document`는 root entrypoint 하나와 20개 Kernel symbol만 +공개한다. `JSONDocument`의 member는 여섯 개다. + +```txt +values 8 +types 12 +total 20 +``` + +패키지는 runtime dependency와 peer dependency가 없다. `/session`과 `/react`는 +export가 아니며, archived 1.x implementation은 production build와 tarball에 +포함하지 않는다. 저장소에 남은 archived source와 regression test는 공개 계약이 +아니다. 별도 `Projection` public type은 외부 구현에서 같은 structural port가 +반복되기 전에는 추가하지 않는다. diff --git a/docs/standard/v2-public-surface.json b/docs/standard/v2-public-surface.json index 5a57abc1..8c665faa 100644 --- a/docs/standard/v2-public-surface.json +++ b/docs/standard/v2-public-surface.json @@ -2,7 +2,17 @@ "formatVersion": 1, "status": "candidate", "sourceContract": "packages/json-document/public-contract.json#root", - "migrationBaselineContract": "packages/json-document/public-contract.json#session", + "package": { + "name": "@interactive-os/json-document", + "version": "2.0.0-rc.0", + "entrypoint": ".", + "excludedEntrypoints": [ + "./session", + "./react" + ], + "runtimeDependencies": [], + "peerDependencies": [] + }, "conformance": { "projectionVectors": "packages/json-document/tests/conformance/v2/projection-vectors.json", "projectionSuite": "packages/json-document/tests/conformance/v2/projection-suite.ts", @@ -41,22 +51,6 @@ "JD2-CONFORMANCE-002", "JD2-BINDING-001" ], - "counts": { - "dispositions": { - "kernel": 16, - "session": 20, - "compat": 14, - "remove": 89 - }, - "documentDispositions": { - "kernel": 6, - "session": 28, - "compat": 5, - "remove": 0 - }, - "baselinePackageSymbols": 139, - "baselineDocumentMembers": 39 - }, "projectionMembers": [ "value", "at", @@ -65,312 +59,7 @@ "commit", "subscribe" ], - "documentMembers": { - "kernel": [ - "value", - "at", - "query", - "canPatch", - "commit", - "subscribe" - ], - "session": [ - "selection", - "history", - "clipboard", - "schema", - "insert", - "replace", - "delete", - "move", - "duplicate", - "copy", - "cut", - "paste", - "undo", - "redo", - "load", - "reset", - "exists", - "entries", - "canInsert", - "canReplace", - "canDelete", - "canMove", - "canDuplicate", - "canCopy", - "canCut", - "canPaste", - "canUndo", - "canRedo" - ], - "compat": [ - "lastPatch", - "patch", - "find", - "canFind", - "canQuery" - ], - "remove": [] - }, - "dispositions": { - "kernel": { - "root": { - "values": [ - "appendSegment", - "applyPatch", - "buildPointer", - "createJSONDocument", - "parentPointer", - "parsePointer", - "trackPointer", - "tryParsePointer" - ], - "types": [ - "JSONCapabilityResult", - "JSONChangeMetadata", - "JSONDocument", - "JSONDocumentCommitOptions", - "JSONPatchOperation", - "Pointer", - "QueryResult", - "ReadResult" - ] - }, - "react": { - "values": [], - "types": [] - } - }, - "session": { - "root": { - "values": [ - "resolveSiblingRange" - ], - "types": [ - "ClipboardCopyResult", - "ClipboardCutResult", - "ClipboardPasteResult", - "ClipboardState", - "JSONDocumentDuplicateOptions", - "JSONDocumentEditResult", - "JSONDocumentHistory", - "JSONDocumentInsertOptions", - "JSONDocumentOptions", - "JSONDocumentPasteOptions", - "JSONDocumentPasteTarget", - "SelectionMode", - "SelectionOptions", - "SelectionPoint", - "SelectionRange", - "SelectionSource", - "SelectionState", - "SiblingRangeResult" - ] - }, - "react": { - "values": [ - "useJSONDocument" - ], - "types": [] - } - }, - "compat": { - "root": { - "values": [ - "JSONDocumentError", - "applyOperation", - "applyPatchToTrustedState", - "lastSegmentIndex", - "withLastSegment" - ], - "types": [ - "ClipboardSource", - "JSONDocumentDuplicateResult", - "JSONDocumentInsertTarget", - "JSONDocumentMoveTarget", - "JSONDocumentSchemaLike", - "JSONDocumentSelectionTarget", - "JSONPatchInput", - "JSONResult", - "SelectionSnap" - ] - }, - "react": { - "values": [], - "types": [] - } - }, - "remove": { - "root": { - "values": [ - "PointerSyntaxError", - "escapeSegment", - "lastSegment", - "replaceTextSurfaceSelection", - "syncTextSurfaceMutation", - "textSurfaceFragment", - "unescapeSegment" - ], - "types": [ - "ClipboardCopyError", - "ClipboardCopyOk", - "ClipboardCopyOptions", - "ClipboardCutError", - "ClipboardCutOk", - "ClipboardCutOptions", - "ClipboardEmpty", - "ClipboardMutationOk", - "ClipboardPasteDiscriminatorMismatch", - "ClipboardPasteError", - "ClipboardReadOk", - "ClipboardReadOptions", - "ClipboardReadResult", - "ClipboardWriteOptions", - "DeleteSelectionTextResult", - "EntriesResult", - "EntryKind", - "HistoryTransactionOptions", - "JSONDocumentDuplicateError", - "JSONDocumentEditError", - "JSONDocumentEditOk", - "JSONDocumentSchemaInput", - "JSONDocumentSchemaOutput", - "ReadEntry", - "ReplaceSelectionTextResult", - "ResolveSiblingRangeOptions", - "SchemaDescription", - "SchemaDescriptionResult", - "SchemaErrorCode", - "SchemaErrorResult", - "SchemaKind", - "SchemaKindResult", - "SchemaPathMode", - "SchemaQueryResult", - "SchemaState", - "SelectionAffinity", - "SelectionContext", - "SelectionCursorDirection", - "SelectionCursorErrorCode", - "SelectionCursorOptions", - "SelectionCursorResult", - "SelectionCursorTarget", - "SelectionDirection", - "SelectionEdge", - "SelectionOrderErrorCode", - "SelectionOrderOptions", - "SelectionOrderedRange", - "SelectionOrderedRangeEntry", - "SelectionPointObject", - "SelectionPointOrderResult", - "SelectionPointerSpan", - "SelectionPointerSpansResult", - "SelectionRangeInput", - "SelectionRangeOrderResult", - "SelectionRangesOrderResult", - "SelectionScopeErrorCode", - "SelectionScopeOptions", - "SelectionScopeResult", - "SelectionScopeTarget", - "SelectionSpanOptions", - "SelectionTextDeleteDirection", - "SelectionTextDeleteOptions", - "SelectionTextEdit", - "SelectionTextEditErrorCode", - "SelectionTextEditOptions", - "SelectionTextEditsResult", - "SelectionType", - "SiblingLocation", - "SiblingRangeErrorCode", - "TextSurface", - "TextSurfaceAtom", - "TextSurfaceError", - "TextSurfaceErrorCode", - "TextSurfaceFragment", - "TextSurfaceFragmentResult", - "TextSurfaceMutationRange", - "TextSurfaceMutationResult", - "TextSurfaceRange", - "TextSurfaceReplaceOptions", - "TextSurfaceReplaceResult", - "TextSurfaceReplacement", - "TextSurfaceSelectionRange" - ] - }, - "react": { - "values": [], - "types": [] - } - } - }, - "compatReplacements": { - "root:values:JSONDocumentError": { - "replacement": "result code plus an optional binding-owned throw helper", - "reason": "Expected execution failure has one canonical result channel." - }, - "root:values:applyOperation": { - "replacement": "root:values:applyPatch", - "reason": "A one-operation batch does not need a second mutation primitive." - }, - "root:values:applyPatchToTrustedState": { - "replacement": "root:values:applyPatch", - "reason": "Trust is an internal optimization and must not create a second public patch meaning." - }, - "root:values:lastSegmentIndex": { - "replacement": "root:values:tryParsePointer plus validation against the addressed container", - "reason": "A numeric final token is not an array index without document context." - }, - "root:values:withLastSegment": { - "replacement": "root:values:parentPointer plus root:values:appendSegment", - "reason": "The operation is a composition of canonical Pointer primitives." - }, - "root:types:ClipboardSource": { - "replacement": "root:types:SelectionSource", - "reason": "Clipboard commands do not need a second name for the same selected source." - }, - "root:types:JSONDocumentDuplicateResult": { - "replacement": "root:types:JSONDocumentEditResult", - "reason": "Duplicate uses the same applied-change result and a non-null target." - }, - "root:types:JSONDocumentInsertTarget": { - "replacement": "proposed:types:JSONDocumentPlacementTarget", - "reason": "Insert and move currently alias the same placement union." - }, - "root:types:JSONDocumentMoveTarget": { - "replacement": "proposed:types:JSONDocumentPlacementTarget", - "reason": "Insert and move currently alias the same placement union." - }, - "root:types:JSONDocumentSchemaLike": { - "replacement": "an unfrozen provider-neutral schema boundary", - "reason": "The current structural name still exposes a Zod-specific contract." - }, - "root:types:JSONDocumentSelectionTarget": { - "replacement": "proposed:types:SelectionSnapshot or root:types:SelectionRange", - "reason": "Commit options should use canonical selection data instead of a document-specific alias." - }, - "root:types:JSONPatchInput": { - "replacement": "ReadonlyArray", - "reason": "The canonical mutation unit is one ordered batch." - }, - "root:types:JSONResult": { - "replacement": "proposed:types:JSONPatchResult or proposed:types:JSONDocumentCommitResult", - "reason": "Pure patch and stateful commit need distinct success payloads instead of one context-free name." - }, - "root:types:SelectionSnap": { - "replacement": "proposed:types:SelectionSnapshot", - "reason": "The durable public name should not abbreviate the established snapshot concept." - } - }, - "documentCompatReplacements": { - "lastPatch": "commit result or subscribed applied change", - "patch": "commit", - "find": "query", - "canFind": "query result", - "canQuery": "query result" - }, "binding": { - "version": "2.0.0-rc.0", - "contract": "packages/json-document/public-contract.json#root", "entrypoint": ".", "values": [ "appendSegment", @@ -395,71 +84,12 @@ "Pointer", "QueryResult", "ReadResult" - ], - "sessionEntrypoint": "./session" + ] }, - "introducedSymbols": [ - { - "entrypoint": "root", - "kind": "types", - "name": "JSONValue", - "disposition": "kernel", - "replaces": [], - "implemented": true - }, - { - "entrypoint": "root", - "kind": "types", - "name": "JSONAppliedChange", - "disposition": "kernel", - "replaces": [ - "root:types:ClipboardMutationOk", - "root:types:JSONDocumentEditOk", - "document:lastPatch" - ], - "implemented": true - }, - { - "entrypoint": "root", - "kind": "types", - "name": "JSONDocumentCommitResult", - "disposition": "kernel", - "replaces": [ - "root:types:JSONResult" - ], - "implemented": true - }, - { - "entrypoint": "root", - "kind": "types", - "name": "JSONPatchResult", - "disposition": "kernel", - "replaces": [ - "root:types:JSONResult" - ], - "implemented": true - }, - { - "entrypoint": "root", - "kind": "types", - "name": "JSONDocumentPlacementTarget", - "disposition": "session", - "replaces": [ - "root:types:JSONDocumentInsertTarget", - "root:types:JSONDocumentMoveTarget" - ], - "implemented": false - }, - { - "entrypoint": "root", - "kind": "types", - "name": "SelectionSnapshot", - "disposition": "session", - "replaces": [ - "root:types:JSONDocumentSelectionTarget", - "root:types:SelectionSnap" - ], - "implemented": false - } - ] + "counts": { + "values": 8, + "types": 12, + "exports": 20, + "projectionMembers": 6 + } } diff --git a/llms.txt b/llms.txt index e397e6c9..132d7f6f 100644 --- a/llms.txt +++ b/llms.txt @@ -43,9 +43,7 @@ React hook, host service를 추가하지 않는다. ## Pure Protocol과 Projection ```txt -Pure Protocol - |-> Document Projection -> host adapter - `-> Candidate Editing Session -> React / rich host adapter +Pure Protocol -> Document Projection -> host adapter ``` Pure Protocol은 JSON value, RFC 6901 JSON Pointer, RFC 9535 JSONPath, @@ -187,42 +185,16 @@ Public Result는 boolean `ok` discriminant를 가진다. Failure는 stable strin Consumer는 exact key 집합이나 exhaustive error-code union에 의존하지 않는다. Minor release에서 optional diagnostic field와 새 code가 추가될 수 있다. -## Candidate Editing Session +## Host adapter -`@interactive-os/json-document/session`은 selection, clipboard, history, schema -introspection과 고수준 edit verb를 가진 Zod 기반 Candidate 표면이다. - -```ts -import { z } from "zod"; -import { - createJSONDocument as createJSONEditingSession, -} from "@interactive-os/json-document/session"; - -const Schema = z.object({ - title: z.string(), - items: z.array(z.object({ id: z.string() })), -}); - -const session = createJSONEditingSession( - Schema, - { title: "Draft", items: [] }, - { history: 100, selection: true }, -); -``` - -이 subpath의 기존 이름과 signature는 v2 stable contract가 아니다. 현재 Session은 -Root `JSONDocument`의 subtype이라고 가정하지 않는다. Portable consumer는 -`/session`에 의존하지 않는다. - -`@interactive-os/json-document/react`의 `useJSONDocument`도 현재 Candidate -Editing Session adapter다. Provider-neutral Projection React adapter로 가정하지 -않는다. +패키지는 root entrypoint만 공개한다. Selection, clipboard, history, schema +introspection, DOM과 framework lifecycle은 host 또는 별도 adapter가 +여섯-member `JSONDocument`를 조합해 소유한다. ## Official extensions -공식 extension은 plugin registry 없이 함수로 조립한다. 현재 package들은 -Candidate Session migration 경계를 사용하며 Root Projection port로의 수렴은 -각 package에서 별도로 검증한다. +공식 extension은 plugin registry 없이 함수로 조립한다. Portable extension은 +Root Projection port를 입력으로 받고, 제품별 기능은 host가 별도로 조합한다. - `@interactive-os/json-document-bulk-edit` - `@interactive-os/json-document-clipboard-web` diff --git a/package-lock.json b/package-lock.json index 7d1fdc2f..19ae4ee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1045,7 +1045,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1402,7 +1401,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1451,7 +1449,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -2473,7 +2470,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2650,7 +2646,6 @@ "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -2660,7 +2655,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3018,7 +3012,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3879,7 +3872,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5173,7 +5165,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5378,7 +5369,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5388,7 +5378,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6203,7 +6192,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6279,7 +6267,6 @@ "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.7", "@vitest/mocker": "4.1.7", @@ -6631,18 +6618,6 @@ "typescript": "^5.0.0", "vitest": "^4.1.7", "zod": "^4.0.0" - }, - "peerDependencies": { - "react": ">=18", - "zod": "^4.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "zod": { - "optional": true - } } }, "packages/outline": { diff --git a/package.json b/package.json index 2149d03e..4dd2be45 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "perf:readiness": "npm run perf:adapters && npm run perf:causal", "perf:core": "npm run build -w @interactive-os/json-document && node scripts/benchmark-core.mjs", "perf:causal": "npm run build -w @interactive-os/json-document && npm run build -w @interactive-os/json-document-patch-rebase && npm run build -w @interactive-os/json-document-stable-id-rebase && npm run build -w @interactive-os/json-document-causal-patch-inbox && node scripts/benchmark-causal.mjs", - "release:check": "npm run verify && npm run standard:check && npm run perf:adapters && npm run perf:core && npm run pack:library", - "standard:check": "node scripts/evaluate-standardization.mjs && npm test -w @interactive-os/json-document -- standard-conformance", + "release:check": "npm run verify -w @interactive-os/json-document && npm run standard:check && npm run docs:evaluate && npm run perf:core && npm run pack:library", + "standard:check": "node scripts/evaluate-standardization.mjs && npm test -w @interactive-os/json-document -- v2-", "verify": "npm run typecheck && npm test && npm run build && npm run smoke:package && npm run build && npm run extensions:verify && npm run labs:extensions:verify && npm run docs:evaluate && npm run playground:typecheck && npm run playground:test && npm run playground:build && npm run site:evaluate && npm run site:verify:pages && npm run browser:test", - "pack:library": "npm pack -w @interactive-os/json-document --cache ./.npm-cache", + "pack:library": "npm pack -w @interactive-os/json-document --dry-run --cache ./.npm-cache", "publish": "npm publish -w @interactive-os/json-document --tag next", "playground:dev": "npm run dev -w @interactive-os/json-document-site", "playground:build": "node scripts/run-workspace-scripts.mjs apps build --if-present", diff --git a/packages/json-document/README.md b/packages/json-document/README.md index 380c1a48..ef6fe448 100644 --- a/packages/json-document/README.md +++ b/packages/json-document/README.md @@ -8,9 +8,7 @@ Patch만 전제로 합니다. UI framework, schema provider, history, selection, clipboard는 core 계약이 아닙니다. ```txt -Pure Protocol - |-> Document Projection -> host adapter - `-> Candidate Editing Session -> React / rich host adapter +Pure Protocol -> Document Projection -> host adapter ``` 현재 버전은 `2.0.0-rc.0`입니다. root v2 binding은 구현됐지만, 독립 구현과 다섯 @@ -28,15 +26,6 @@ Core만 쓸 때 필수 dependency가 없습니다. npm install @interactive-os/json-document@2.0.0-rc.0 ``` -기존의 Zod 기반 selection, clipboard, history 편집기는 optional Session -binding입니다. - -```sh -npm install @interactive-os/json-document@2.0.0-rc.0 zod -``` - -React는 Session을 사용할 때만 필요합니다. - ## 60초 시작 ```ts @@ -137,69 +126,10 @@ types ReadResult, QueryResult, JSONDocument ``` -`JSONDocument`는 application-owned structural contract입니다. Candidate -Session은 아직 이 구조의 subtype이라고 약속하지 않는 별도 변경 경계입니다. - -## Editing Session - -Selection, clipboard, history, schema introspection과 `insert`, `replace`, -`delete`, `move`, `duplicate`, `copy`, `cut`, `paste`, `undo`, `redo`는 optional -Editing Session입니다. - -```ts -import { z } from "zod"; -import { createJSONDocument } from "@interactive-os/json-document/session"; - -const Schema = z.object({ - title: z.string(), - tasks: z.array(z.object({ id: z.string(), done: z.boolean() })), -}); - -const session = createJSONDocument( - Schema, - { title: "Draft", tasks: [] }, - { history: 100, selection: true }, -); - -session.insert("/tasks/-", { id: "a", done: false }); -session.undo(); -``` - -`/session`은 rich editor 기능의 별도 변경 경계입니다. portable Core consumer는 -이 subpath에 의존하지 않습니다. - -## React — `useJSONDocument` - -```tsx -import { z } from "zod"; -import { useJSONDocument } from "@interactive-os/json-document/react"; - -const Schema = z.object({ - title: z.string(), -}); - -export function App() { - const document = useJSONDocument( - Schema, - { title: "Draft" }, - { history: 20 }, - ); - - return ( - { - document.commit([ - { op: "replace", path: "/title", value: event.target.value }, - ]); - }} - /> - ); -} -``` - -Root import graph에는 React와 Zod가 없습니다. React hook은 Editing Session -binding에만 연결됩니다. +`JSONDocument`는 application-owned structural contract입니다. Selection, +history, clipboard, DOM lifecycle과 framework binding은 별도 host 또는 adapter가 +이 여섯 member를 조합합니다. 패키지는 `/session`이나 `/react` subpath를 +공개하지 않습니다. ## 순수 core diff --git a/packages/json-document/package.json b/packages/json-document/package.json index 596926d1..d0d4ff1f 100644 --- a/packages/json-document/package.json +++ b/packages/json-document/package.json @@ -41,14 +41,6 @@ ".": { "types": "./dist/application/document/index.d.ts", "import": "./dist/application/document/index.js" - }, - "./session": { - "types": "./dist/application/session/index.d.ts", - "import": "./dist/application/session/index.js" - }, - "./react": { - "types": "./dist/application/react-document/index.d.ts", - "import": "./dist/application/react-document/index.js" } }, "scripts": { @@ -62,18 +54,6 @@ "typecheck": "tsc -p tsconfig.test.json --noEmit", "verify": "npm run layers:check && npm run typecheck && npm test && npm run build && npm run smoke:package" }, - "peerDependencies": { - "react": ">=18", - "zod": "^4.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "zod": { - "optional": true - } - }, "devDependencies": { "@types/node": "^25.9.0", "@types/react": "^19.0.0", diff --git a/packages/json-document/public-contract.json b/packages/json-document/public-contract.json index 8776fc05..938251ff 100644 --- a/packages/json-document/public-contract.json +++ b/packages/json-document/public-contract.json @@ -24,155 +24,5 @@ "QueryResult", "ReadResult" ] - }, - "session": { - "values": [ - "JSONDocumentError", - "PointerSyntaxError", - "appendSegment", - "applyOperation", - "applyPatch", - "applyPatchToTrustedState", - "buildPointer", - "createJSONDocument", - "escapeSegment", - "lastSegment", - "lastSegmentIndex", - "parentPointer", - "parsePointer", - "replaceTextSurfaceSelection", - "resolveSiblingRange", - "syncTextSurfaceMutation", - "textSurfaceFragment", - "trackPointer", - "tryParsePointer", - "unescapeSegment", - "withLastSegment" - ], - "types": [ - "ClipboardCopyError", - "ClipboardCopyOk", - "ClipboardCopyOptions", - "ClipboardCopyResult", - "ClipboardCutError", - "ClipboardCutOk", - "ClipboardCutOptions", - "ClipboardCutResult", - "ClipboardEmpty", - "ClipboardMutationOk", - "ClipboardPasteDiscriminatorMismatch", - "ClipboardPasteError", - "ClipboardPasteResult", - "ClipboardReadOk", - "ClipboardReadOptions", - "ClipboardReadResult", - "ClipboardSource", - "ClipboardState", - "ClipboardWriteOptions", - "DeleteSelectionTextResult", - "EntriesResult", - "EntryKind", - "HistoryTransactionOptions", - "JSONCapabilityResult", - "JSONChangeMetadata", - "JSONDocument", - "JSONDocumentCommitOptions", - "JSONDocumentDuplicateError", - "JSONDocumentDuplicateOptions", - "JSONDocumentDuplicateResult", - "JSONDocumentEditError", - "JSONDocumentEditOk", - "JSONDocumentEditResult", - "JSONDocumentHistory", - "JSONDocumentInsertOptions", - "JSONDocumentInsertTarget", - "JSONDocumentMoveTarget", - "JSONDocumentOptions", - "JSONDocumentPasteOptions", - "JSONDocumentPasteTarget", - "JSONDocumentSchemaInput", - "JSONDocumentSchemaLike", - "JSONDocumentSchemaOutput", - "JSONDocumentSelectionTarget", - "JSONPatchInput", - "JSONPatchOperation", - "JSONResult", - "Pointer", - "QueryResult", - "ReadEntry", - "ReadResult", - "ReplaceSelectionTextResult", - "ResolveSiblingRangeOptions", - "SchemaDescription", - "SchemaDescriptionResult", - "SchemaErrorCode", - "SchemaErrorResult", - "SchemaKind", - "SchemaKindResult", - "SchemaPathMode", - "SchemaQueryResult", - "SchemaState", - "SelectionAffinity", - "SelectionContext", - "SelectionCursorDirection", - "SelectionCursorErrorCode", - "SelectionCursorOptions", - "SelectionCursorResult", - "SelectionCursorTarget", - "SelectionDirection", - "SelectionEdge", - "SelectionMode", - "SelectionOptions", - "SelectionOrderErrorCode", - "SelectionOrderOptions", - "SelectionOrderedRange", - "SelectionOrderedRangeEntry", - "SelectionPoint", - "SelectionPointObject", - "SelectionPointOrderResult", - "SelectionPointerSpan", - "SelectionPointerSpansResult", - "SelectionRange", - "SelectionRangeInput", - "SelectionRangeOrderResult", - "SelectionRangesOrderResult", - "SelectionScopeErrorCode", - "SelectionScopeOptions", - "SelectionScopeResult", - "SelectionScopeTarget", - "SelectionSnap", - "SelectionSource", - "SelectionSpanOptions", - "SelectionState", - "SelectionTextDeleteDirection", - "SelectionTextDeleteOptions", - "SelectionTextEdit", - "SelectionTextEditErrorCode", - "SelectionTextEditOptions", - "SelectionTextEditsResult", - "SelectionType", - "SiblingLocation", - "SiblingRangeErrorCode", - "SiblingRangeResult", - "TextSurface", - "TextSurfaceAtom", - "TextSurfaceError", - "TextSurfaceErrorCode", - "TextSurfaceFragment", - "TextSurfaceFragmentResult", - "TextSurfaceMutationRange", - "TextSurfaceMutationResult", - "TextSurfaceRange", - "TextSurfaceReplaceOptions", - "TextSurfaceReplaceResult", - "TextSurfaceReplacement", - "TextSurfaceSelectionRange" - ] - }, - "react": { - "values": [ - "useJSONDocument" - ], - "types": [] } } diff --git a/packages/json-document/src/foundation/json/clone.ts b/packages/json-document/src/foundation/json/clone.ts index cbda7cc6..8c7f83c1 100644 --- a/packages/json-document/src/foundation/json/clone.ts +++ b/packages/json-document/src/foundation/json/clone.ts @@ -1,4 +1,4 @@ -import { buildPointer } from "../pointer/index.js"; +import { buildPointer } from "../pointer/core.js"; import { cloneTrustedPlainJson } from "./trustedClone.js"; import { jsonSerializableErrorFast } from "./serializable.js"; diff --git a/packages/json-document/src/foundation/json/serializable.ts b/packages/json-document/src/foundation/json/serializable.ts index 5ddf2417..582f9593 100644 --- a/packages/json-document/src/foundation/json/serializable.ts +++ b/packages/json-document/src/foundation/json/serializable.ts @@ -1,4 +1,4 @@ -import { buildPointer } from "../pointer/index.js"; +import { buildPointer } from "../pointer/core.js"; export function jsonSerializableError(value: unknown): string | null { return jsonSerializableErrorFast(value) === null ? null : jsonSerializableErrorDetailed(value); diff --git a/packages/json-document/src/foundation/jsonpath/ast.ts b/packages/json-document/src/foundation/jsonpath/ast.ts index 85bf402c..28c1364a 100644 --- a/packages/json-document/src/foundation/jsonpath/ast.ts +++ b/packages/json-document/src/foundation/jsonpath/ast.ts @@ -1,7 +1,7 @@ // foundation/jsonpath — RFC 9535 JSONPath types. // AST 정의. tokenizer / parser / evaluator 가 공유. -import type { Pointer } from "../pointer/index.js"; +import type { Pointer } from "../pointer/core.js"; /** RFC 9535 query AST root. `$` + segments. */ export interface Query { diff --git a/packages/json-document/src/foundation/jsonpath/index.ts b/packages/json-document/src/foundation/jsonpath/index.ts index 8f624fcd..6c406b18 100644 --- a/packages/json-document/src/foundation/jsonpath/index.ts +++ b/packages/json-document/src/foundation/jsonpath/index.ts @@ -14,7 +14,7 @@ import { parse as parseJsonPath } from "./parse.js"; import { evaluate, matchPointers } from "./evaluate.js"; import { matchPointersForSimpleQuery } from "./simple.js"; -import type { Pointer } from "../pointer/index.js"; +import type { Pointer } from "../pointer/core.js"; import type { Match, Query } from "./ast.js"; export { parse } from "./parse.js"; export { JSONPathSyntaxError } from "./tokenize.js"; diff --git a/packages/json-document/src/foundation/patch/apply.ts b/packages/json-document/src/foundation/patch/apply.ts index 22483355..3561fd3f 100644 --- a/packages/json-document/src/foundation/patch/apply.ts +++ b/packages/json-document/src/foundation/patch/apply.ts @@ -1,6 +1,6 @@ // applyOpRaw — RFC 6902 6 op 의 raw 적용 (schema 검증 없음). public 노출은 patch.ts. -import { isPrefix, parsePointer, type Pointer } from "../pointer/index.js"; +import { isPrefix, parsePointer, type Pointer } from "../pointer/core.js"; import { cloneJson } from "../json/clone.js"; import { jsonSerializableError } from "../json/serializable.js"; import type { ErrorCode, JSONPatchOperation } from "./contract.js"; diff --git a/packages/json-document/src/foundation/patch/container.ts b/packages/json-document/src/foundation/patch/container.ts index ca413baf..2c1dde4a 100644 --- a/packages/json-document/src/foundation/patch/container.ts +++ b/packages/json-document/src/foundation/patch/container.ts @@ -1,6 +1,6 @@ // patch.ts 내부 헬퍼 — public API 아님. docs/standard/json-document-spec.md §3 의 RFC 6902 구현 디테일. -import { parsePointer, readAt, type Pointer, PointerSyntaxError } from "../pointer/index.js"; +import { parsePointer, readAt, type Pointer, PointerSyntaxError } from "../pointer/core.js"; import type { ErrorCode, JSONPatchOperation } from "./contract.js"; // RFC 6902 §4.1: `/-` 는 array append marker. 적용 시점의 array 길이로 concrete index 정규화. diff --git a/packages/json-document/src/foundation/patch/contract.ts b/packages/json-document/src/foundation/patch/contract.ts index b82c1fe8..825f98d7 100644 --- a/packages/json-document/src/foundation/patch/contract.ts +++ b/packages/json-document/src/foundation/patch/contract.ts @@ -1,5 +1,4 @@ -import type * as z from "zod"; -import type { Pointer } from "../pointer/index.js"; +import type { Pointer } from "../pointer/core.js"; export type JSONPatchOperation = | { op: "add"; path: Pointer; value: unknown } @@ -21,12 +20,6 @@ export type JSONResult = | { ok: true } | { ok: false; code: ErrorCode; reason?: string; pointer?: Pointer }; -export interface ApplyResult { - state: z.output; - result: JSONResult; - applied: ReadonlyArray; -} - export interface TrustedApplyResult { state: T; result: JSONResult; diff --git a/packages/json-document/src/foundation/patch/fast/array.ts b/packages/json-document/src/foundation/patch/fast/array.ts index 10967ee4..d82253c5 100644 --- a/packages/json-document/src/foundation/patch/fast/array.ts +++ b/packages/json-document/src/foundation/patch/fast/array.ts @@ -1,6 +1,6 @@ import { jsonSerializableError } from "../../json/serializable.js"; import { cloneTrustedPlainJson } from "../../json/trustedClone.js"; -import { appendSegment, type Pointer } from "../../pointer/index.js"; +import { appendSegment, type Pointer } from "../../pointer/core.js"; import { getValueAt, parseSafe } from "../container.js"; import { appendArrayIndexPath, arrayLocation, arrayRemoveLocation } from "../path.js"; import { replaceValueAtSegments } from "../replaceValue.js"; diff --git a/packages/json-document/src/foundation/patch/fast/replace.ts b/packages/json-document/src/foundation/patch/fast/replace.ts index 0a1bd6f9..ac1404eb 100644 --- a/packages/json-document/src/foundation/patch/fast/replace.ts +++ b/packages/json-document/src/foundation/patch/fast/replace.ts @@ -1,5 +1,5 @@ import { jsonSerializableError } from "../../json/serializable.js"; -import type { Pointer } from "../../pointer/index.js"; +import type { Pointer } from "../../pointer/core.js"; import { getValueAt, parseSafe } from "../container.js"; import { objectHasOwn } from "../object.js"; import { diff --git a/packages/json-document/src/foundation/patch/index.ts b/packages/json-document/src/foundation/patch/index.ts index 7a1cb96b..969d7e75 100644 --- a/packages/json-document/src/foundation/patch/index.ts +++ b/packages/json-document/src/foundation/patch/index.ts @@ -5,7 +5,6 @@ export { } from "./apply.js"; export type { RawResult } from "./apply.js"; export type { - ApplyResult, ErrorCode, FastPatchResult, JSONPatchOperation, @@ -13,6 +12,7 @@ export type { TrustedApplyResult, TrustedPatchOptions, } from "./contract.js"; +export type { ApplyResult } from "./schema-contract.js"; export { computeInverses } from "./inverse.js"; export { copyRootObject, @@ -42,7 +42,6 @@ export { replaceValueAtSegments } from "./replaceValue.js"; export { fail, ok, - zodIssuesReason, } from "./result.js"; export { applyOperation, diff --git a/packages/json-document/src/foundation/patch/inverse.ts b/packages/json-document/src/foundation/patch/inverse.ts index d5b35a08..78b417d1 100644 --- a/packages/json-document/src/foundation/patch/inverse.ts +++ b/packages/json-document/src/foundation/patch/inverse.ts @@ -1,7 +1,7 @@ // computeInverses — undo 용 RFC 6902 inverse op 계산. import type { JSONPatchOperation } from "./contract.js"; -import { appendSegment, parsePointer, readAt } from "../pointer/index.js"; +import { appendSegment, parsePointer, readAt } from "../pointer/core.js"; import { cloneTrustedPlainJson } from "../json/trustedClone.js"; import { applyOpRaw } from "./apply.js"; import { getValueAt, resolveAppendPath } from "./container.js"; diff --git a/packages/json-document/src/foundation/patch/path.ts b/packages/json-document/src/foundation/patch/path.ts index 854d0708..dff31c51 100644 --- a/packages/json-document/src/foundation/patch/path.ts +++ b/packages/json-document/src/foundation/patch/path.ts @@ -1,4 +1,4 @@ -import { buildPointer, parentPointer, type Pointer } from "../pointer/index.js"; +import { buildPointer, parentPointer, type Pointer } from "../pointer/core.js"; import { getValueAt, parseSafe } from "./container.js"; export interface ArrayFieldPath { diff --git a/packages/json-document/src/foundation/patch/result.ts b/packages/json-document/src/foundation/patch/result.ts index 575437de..336e5b14 100644 --- a/packages/json-document/src/foundation/patch/result.ts +++ b/packages/json-document/src/foundation/patch/result.ts @@ -1,5 +1,4 @@ -import type * as z from "zod"; -import type { Pointer } from "../pointer/index.js"; +import type { Pointer } from "../pointer/core.js"; import type { ErrorCode, JSONResult } from "./contract.js"; export const ok: JSONResult = { ok: true }; @@ -7,5 +6,3 @@ export const ok: JSONResult = { ok: true }; export function fail(code: ErrorCode, reason?: string, pointer?: Pointer): JSONResult { return { ok: false, code, ...(reason === undefined ? {} : { reason }), ...(pointer === undefined ? {} : { pointer }) }; } - -export const zodIssuesReason = (error: z.ZodError): string => JSON.stringify(error.issues); diff --git a/packages/json-document/src/foundation/patch/schema-contract.ts b/packages/json-document/src/foundation/patch/schema-contract.ts new file mode 100644 index 00000000..5e318b45 --- /dev/null +++ b/packages/json-document/src/foundation/patch/schema-contract.ts @@ -0,0 +1,11 @@ +import type * as z from "zod"; +import type { + JSONPatchOperation, + JSONResult, +} from "./contract.js"; + +export interface ApplyResult { + state: z.output; + result: JSONResult; + applied: ReadonlyArray; +} diff --git a/packages/json-document/src/foundation/patch/schema.ts b/packages/json-document/src/foundation/patch/schema.ts index 013233e1..5c6dd513 100644 --- a/packages/json-document/src/foundation/patch/schema.ts +++ b/packages/json-document/src/foundation/patch/schema.ts @@ -9,9 +9,12 @@ import { } from "./apply.js"; import { normalizeAppliedOp, normalizeOp } from "./container.js"; import { applyFastPatchStrategies, publicTrustedStateStrategies } from "./fast/apply.js"; -import { fail, ok, zodIssuesReason } from "./result.js"; +import { fail, ok } from "./result.js"; import { applyTrustedValueMutation } from "./value.js"; -import type { ApplyResult, JSONPatchOperation } from "./contract.js"; +import type { JSONPatchOperation } from "./contract.js"; +import type { ApplyResult } from "./schema-contract.js"; + +const zodIssuesReason = (error: z.ZodError): string => JSON.stringify(error.issues); export function applyOperation( schema: S, diff --git a/packages/json-document/src/foundation/patch/source.ts b/packages/json-document/src/foundation/patch/source.ts index 00cc802c..41316add 100644 --- a/packages/json-document/src/foundation/patch/source.ts +++ b/packages/json-document/src/foundation/patch/source.ts @@ -1,5 +1,5 @@ import type { JSONPatchOperation } from "./contract.js"; -import { tryParsePointer, type Pointer } from "../pointer/index.js"; +import { tryParsePointer, type Pointer } from "../pointer/core.js"; import { normalizePointerSources, type PointerSource, diff --git a/packages/json-document/src/foundation/patch/track.ts b/packages/json-document/src/foundation/patch/track.ts index da0a1d3e..92a41f4d 100644 --- a/packages/json-document/src/foundation/patch/track.ts +++ b/packages/json-document/src/foundation/patch/track.ts @@ -2,7 +2,7 @@ // 입력: 적용된 op + 기존 Pointer // 출력: 새 Pointer (또는 null = cascading drop) -import { tryParsePointer, buildPointer, isPrefix, parentPointer, lastSegmentIndex, withLastSegment, readAt, type Pointer } from "../pointer/index.js"; +import { tryParsePointer, buildPointer, isPrefix, parentPointer, lastSegmentIndex, withLastSegment, readAt, type Pointer } from "../pointer/core.js"; import type { JSONPatchOperation } from "./contract.js"; import { arrayIndexValue } from "../pointer/array.js"; diff --git a/packages/json-document/src/foundation/patch/value.ts b/packages/json-document/src/foundation/patch/value.ts index c19a2596..2caf4796 100644 --- a/packages/json-document/src/foundation/patch/value.ts +++ b/packages/json-document/src/foundation/patch/value.ts @@ -1,4 +1,4 @@ -import type { Pointer } from "../pointer/index.js"; +import type { Pointer } from "../pointer/core.js"; import { mutateContainer, parseSafe, diff --git a/packages/json-document/src/foundation/pointer/array.ts b/packages/json-document/src/foundation/pointer/array.ts index 94332d75..209c320f 100644 --- a/packages/json-document/src/foundation/pointer/array.ts +++ b/packages/json-document/src/foundation/pointer/array.ts @@ -1,4 +1,4 @@ -import { buildPointer, tryParsePointer, type Pointer } from "./index.js"; +import { buildPointer, tryParsePointer, type Pointer } from "./core.js"; export function arrayIndexValue(seg: string): number | null { if (seg === "0") return 0; diff --git a/packages/json-document/src/foundation/pointer/core.ts b/packages/json-document/src/foundation/pointer/core.ts new file mode 100644 index 00000000..a6076bb7 --- /dev/null +++ b/packages/json-document/src/foundation/pointer/core.ts @@ -0,0 +1,179 @@ +// RFC 6901 — JSON Pointer. +// 정본: docs/standard/json-document-spec.md §2. 변환은 lossless. + +export type Pointer = string; + +export function escapeSegment(s: string): string { + if (!s.includes("~") && !s.includes("/")) return s; + return s.replace(/~/g, "~0").replace(/\//g, "~1"); +} + +export function unescapeSegment(s: string): string { + if (!s.includes("~")) return s; + return s.replace(/~1/g, "/").replace(/~0/g, "~"); +} + +export interface BuildPointerOptions { + /** RFC 6901 §6 — URI fragment 표현 (`#` prefix + percent-encoding). default false. */ + uriFragment?: boolean; +} + +export function buildPointer( + segments: ReadonlyArray, + options: BuildPointerOptions = {}, +): Pointer { + if (segments.length === 0) return options.uriFragment ? "#" : ""; + let body = ""; + for (let index = 0; index < segments.length; index += 1) { + body += "/" + escapeSegment(String(segments[index])); + } + return options.uriFragment ? "#" + encodePointerForFragment(body) : body; +} + +function parsePointerSegments(body: string): string[] { + if (!body.includes("~")) return body.split("/"); + return body.split("/").map((segment) => { + for (let index = segment.indexOf("~"); index !== -1; index = segment.indexOf("~", index + 2)) { + const escaped = segment[index + 1]; + if (escaped !== "0" && escaped !== "1") { + throw new PointerSyntaxError(`Invalid JSON Pointer escape in segment: ${JSON.stringify(segment)}`); + } + } + return unescapeSegment(segment); + }); +} + +export function parsePointer(pointer: Pointer): string[] { + if (pointer === "" || pointer === "#") return []; + // RFC 6901 §6 — URI fragment 형식 (`#/foo`) 자동 디코드. + if (pointer[0] === "#") { + if (pointer[1] !== "/") { + throw new PointerSyntaxError(`JSON Pointer URI fragment must be '#' or start with '#/': ${JSON.stringify(pointer)}`); + } + let decoded: string; + try { + decoded = decodeURIComponent(pointer.slice(2)); + } catch (error) { + throw new PointerSyntaxError( + error instanceof Error + ? `Invalid JSON Pointer URI fragment encoding: ${error.message}` + : "Invalid JSON Pointer URI fragment encoding", + ); + } + return parsePointerSegments(decoded); + } + if (pointer[0] !== "/") { + throw new PointerSyntaxError(`JSON Pointer must be empty or start with '/': ${JSON.stringify(pointer)}`); + } + return parsePointerSegments(pointer.slice(1)); +} + +export function tryParsePointer(pointer: Pointer): string[] | null { + if (pointer === "" || pointer === "#") return []; + if (pointer[0] === "#") { + if (pointer[1] !== "/") return null; + try { + return parsePointerSegments(decodeURIComponent(pointer.slice(2))); + } catch { + return null; + } + } + if (pointer[0] !== "/") return null; + try { + return parsePointerSegments(pointer.slice(1)); + } catch { + return null; + } +} + +// RFC 3986 + 6901 §6: fragment 안에서 안전하지 않은 문자 percent-encode. +// JSON Pointer 자체의 escape (~0, ~1) 는 이미 처리됐으므로 fragment 의 추가 제약만. +function encodePointerForFragment(s: string): string { + // encodeURI는 Unicode를 UTF-8 octet으로 바꾼 뒤 percent-encode한다. + // Fragment delimiter인 #만 JSON Pointer 본문에 남지 않도록 추가 인코드한다. + return encodeURI(s).replace(/#/g, "%23"); +} + +export class PointerSyntaxError extends Error { + override readonly name = "PointerSyntaxError"; +} + +// ── Path arithmetic (state-free, schema-free) ─────────────────────────────── +// SPEC §5.6. RFC 6901 위에서 순수 path 조작. state·schema 모름. 모든 editor 가 공유. + +/** Parent pointer. `""` (root) 는 `null`. `"/a"` → `""`, `"/a/b"` → `"/a"`. */ +export function parentPointer(pointer: Pointer): Pointer | null { + if (pointer === "") return null; + const i = pointer.lastIndexOf("/"); + return i <= 0 ? "" : pointer.slice(0, i); +} + +/** 마지막 segment (이스케이프 디코드된). `""` 또는 `null`. */ +export function lastSegment(pointer: Pointer): string | null { + if (pointer === "") return null; + const i = pointer.lastIndexOf("/"); + if (i < 0) return null; + return unescapeSegment(pointer.slice(i + 1)); +} + +/** 마지막 segment 가 array index 면 그 정수, 아니면 `null` (record key 또는 root). */ +export function lastSegmentIndex(pointer: Pointer): number | null { + const seg = lastSegment(pointer); + if (seg === null) return null; + if (!isArrayIndexSegment(seg)) return null; + return Number(seg); +} + +function isArrayIndexSegment(segment: string): boolean { + if (segment === "") return false; + if (segment === "0") return true; + const first = segment.charCodeAt(0); + if (first < 49 || first > 57) return false; + for (let index = 1; index < segment.length; index += 1) { + const code = segment.charCodeAt(index); + if (code < 48 || code > 57) return false; + } + return true; +} + +/** Pointer 끝에 segment 추가. `appendSegment("/a", 0)` → `"/a/0"`, escape 자동. */ +export function appendSegment(pointer: Pointer, seg: string | number): Pointer { + return pointer + "/" + escapeSegment(String(seg)); +} + +/** Pointer 의 마지막 segment 를 교체. root 면 `null`. */ +export function withLastSegment(pointer: Pointer, seg: string | number): Pointer | null { + if (pointer === "") return null; + const i = pointer.lastIndexOf("/"); + if (i < 0) return null; + return pointer.slice(0, i + 1) + escapeSegment(String(seg)); +} + +// ── Internal helpers (not in public index) ────────────────────────────────── + +/** segs prefix check. */ +export function isPrefix(prefix: ReadonlyArray, full: ReadonlyArray): boolean { + if (prefix.length > full.length) return false; + for (let i = 0; i < prefix.length; i++) if (prefix[i] !== full[i]) return false; + return true; +} + +/** state + segments → value. boolean ok. `"-"` 는 path_not_found 로 취급. */ +export function readAt(state: unknown, segs: ReadonlyArray): { ok: true; value: unknown } | { ok: false } { + let cur: unknown = state; + for (const seg of segs) { + if (cur === null || typeof cur !== "object") return { ok: false }; + if (Array.isArray(cur)) { + // RFC 6901 §4: 배열 인덱스는 `0` 또는 `[1-9][0-9]*` 만 허용. write path 와 + // 동일한 strict 파서를 써서 1.0/01/+1/"-" 같은 non-canonical 토큰을 거부한다. + if (!isArrayIndexSegment(seg)) return { ok: false }; + const i = Number(seg); + if (i >= cur.length) return { ok: false }; + cur = cur[i]; + } else { + if (!Object.prototype.hasOwnProperty.call(cur, seg)) return { ok: false }; + cur = (cur as Record)[seg]; + } + } + return { ok: true, value: cur }; +} diff --git a/packages/json-document/src/foundation/pointer/index.ts b/packages/json-document/src/foundation/pointer/index.ts index fb799e03..00bccb01 100644 --- a/packages/json-document/src/foundation/pointer/index.ts +++ b/packages/json-document/src/foundation/pointer/index.ts @@ -1,182 +1,22 @@ -// RFC 6901 — JSON Pointer. -// 정본: docs/standard/json-document-spec.md §2. 변환은 lossless. - -export type Pointer = string; - -export function escapeSegment(s: string): string { - if (!s.includes("~") && !s.includes("/")) return s; - return s.replace(/~/g, "~0").replace(/\//g, "~1"); -} - -export function unescapeSegment(s: string): string { - if (!s.includes("~")) return s; - return s.replace(/~1/g, "/").replace(/~0/g, "~"); -} - -export interface BuildPointerOptions { - /** RFC 6901 §6 — URI fragment 표현 (`#` prefix + percent-encoding). default false. */ - uriFragment?: boolean; -} - -export function buildPointer( - segments: ReadonlyArray, - options: BuildPointerOptions = {}, -): Pointer { - if (segments.length === 0) return options.uriFragment ? "#" : ""; - let body = ""; - for (let index = 0; index < segments.length; index += 1) { - body += "/" + escapeSegment(String(segments[index])); - } - return options.uriFragment ? "#" + encodePointerForFragment(body) : body; -} - -function parsePointerSegments(body: string): string[] { - if (!body.includes("~")) return body.split("/"); - return body.split("/").map((segment) => { - for (let index = segment.indexOf("~"); index !== -1; index = segment.indexOf("~", index + 2)) { - const escaped = segment[index + 1]; - if (escaped !== "0" && escaped !== "1") { - throw new PointerSyntaxError(`Invalid JSON Pointer escape in segment: ${JSON.stringify(segment)}`); - } - } - return unescapeSegment(segment); - }); -} - -export function parsePointer(pointer: Pointer): string[] { - if (pointer === "" || pointer === "#") return []; - // RFC 6901 §6 — URI fragment 형식 (`#/foo`) 자동 디코드. - if (pointer[0] === "#") { - if (pointer[1] !== "/") { - throw new PointerSyntaxError(`JSON Pointer URI fragment must be '#' or start with '#/': ${JSON.stringify(pointer)}`); - } - let decoded: string; - try { - decoded = decodeURIComponent(pointer.slice(2)); - } catch (error) { - throw new PointerSyntaxError( - error instanceof Error - ? `Invalid JSON Pointer URI fragment encoding: ${error.message}` - : "Invalid JSON Pointer URI fragment encoding", - ); - } - return parsePointerSegments(decoded); - } - if (pointer[0] !== "/") { - throw new PointerSyntaxError(`JSON Pointer must be empty or start with '/': ${JSON.stringify(pointer)}`); - } - return parsePointerSegments(pointer.slice(1)); -} - -export function tryParsePointer(pointer: Pointer): string[] | null { - if (pointer === "" || pointer === "#") return []; - if (pointer[0] === "#") { - if (pointer[1] !== "/") return null; - try { - return parsePointerSegments(decodeURIComponent(pointer.slice(2))); - } catch { - return null; - } - } - if (pointer[0] !== "/") return null; - try { - return parsePointerSegments(pointer.slice(1)); - } catch { - return null; - } -} - -// RFC 3986 + 6901 §6: fragment 안에서 안전하지 않은 문자 percent-encode. -// JSON Pointer 자체의 escape (~0, ~1) 는 이미 처리됐으므로 fragment 의 추가 제약만. -function encodePointerForFragment(s: string): string { - // encodeURI는 Unicode를 UTF-8 octet으로 바꾼 뒤 percent-encode한다. - // Fragment delimiter인 #만 JSON Pointer 본문에 남지 않도록 추가 인코드한다. - return encodeURI(s).replace(/#/g, "%23"); -} - -export class PointerSyntaxError extends Error { - override readonly name = "PointerSyntaxError"; -} - -// ── Path arithmetic (state-free, schema-free) ─────────────────────────────── -// SPEC §5.6. RFC 6901 위에서 순수 path 조작. state·schema 모름. 모든 editor 가 공유. - -/** Parent pointer. `""` (root) 는 `null`. `"/a"` → `""`, `"/a/b"` → `"/a"`. */ -export function parentPointer(pointer: Pointer): Pointer | null { - if (pointer === "") return null; - const i = pointer.lastIndexOf("/"); - return i <= 0 ? "" : pointer.slice(0, i); -} - -/** 마지막 segment (이스케이프 디코드된). `""` 또는 `null`. */ -export function lastSegment(pointer: Pointer): string | null { - if (pointer === "") return null; - const i = pointer.lastIndexOf("/"); - if (i < 0) return null; - return unescapeSegment(pointer.slice(i + 1)); -} - -/** 마지막 segment 가 array index 면 그 정수, 아니면 `null` (record key 또는 root). */ -export function lastSegmentIndex(pointer: Pointer): number | null { - const seg = lastSegment(pointer); - if (seg === null) return null; - if (!isArrayIndexSegment(seg)) return null; - return Number(seg); -} - -function isArrayIndexSegment(segment: string): boolean { - if (segment === "") return false; - if (segment === "0") return true; - const first = segment.charCodeAt(0); - if (first < 49 || first > 57) return false; - for (let index = 1; index < segment.length; index += 1) { - const code = segment.charCodeAt(index); - if (code < 48 || code > 57) return false; - } - return true; -} - -/** Pointer 끝에 segment 추가. `appendSegment("/a", 0)` → `"/a/0"`, escape 자동. */ -export function appendSegment(pointer: Pointer, seg: string | number): Pointer { - return pointer + "/" + escapeSegment(String(seg)); -} - -/** Pointer 의 마지막 segment 를 교체. root 면 `null`. */ -export function withLastSegment(pointer: Pointer, seg: string | number): Pointer | null { - if (pointer === "") return null; - const i = pointer.lastIndexOf("/"); - if (i < 0) return null; - return pointer.slice(0, i + 1) + escapeSegment(String(seg)); -} - -// ── Internal helpers (not in public index) ────────────────────────────────── - -/** segs prefix check. */ -export function isPrefix(prefix: ReadonlyArray, full: ReadonlyArray): boolean { - if (prefix.length > full.length) return false; - for (let i = 0; i < prefix.length; i++) if (prefix[i] !== full[i]) return false; - return true; -} - -/** state + segments → value. boolean ok. `"-"` 는 path_not_found 로 취급. */ -export function readAt(state: unknown, segs: ReadonlyArray): { ok: true; value: unknown } | { ok: false } { - let cur: unknown = state; - for (const seg of segs) { - if (cur === null || typeof cur !== "object") return { ok: false }; - if (Array.isArray(cur)) { - // RFC 6901 §4: 배열 인덱스는 `0` 또는 `[1-9][0-9]*` 만 허용. write path 와 - // 동일한 strict 파서를 써서 1.0/01/+1/"-" 같은 non-canonical 토큰을 거부한다. - if (!isArrayIndexSegment(seg)) return { ok: false }; - const i = Number(seg); - if (i >= cur.length) return { ok: false }; - cur = cur[i]; - } else { - if (!Object.prototype.hasOwnProperty.call(cur, seg)) return { ok: false }; - cur = (cur as Record)[seg]; - } - } - return { ok: true, value: cur }; -} +export { + PointerSyntaxError, + appendSegment, + buildPointer, + escapeSegment, + isPrefix, + lastSegment, + lastSegmentIndex, + parentPointer, + parsePointer, + readAt, + tryParsePointer, + unescapeSegment, + withLastSegment, +} from "./core.js"; +export type { + BuildPointerOptions, + Pointer, +} from "./core.js"; export { appendArrayIndexes, diff --git a/packages/json-document/src/foundation/pointer/siblingRange.ts b/packages/json-document/src/foundation/pointer/siblingRange.ts index 5a8fa9ac..2bf88e03 100644 --- a/packages/json-document/src/foundation/pointer/siblingRange.ts +++ b/packages/json-document/src/foundation/pointer/siblingRange.ts @@ -12,7 +12,7 @@ import { parentPointer, tryParsePointer, type Pointer, -} from "./index.js"; +} from "./core.js"; export type SiblingRangeErrorCode = | "empty_selection" diff --git a/packages/json-document/src/foundation/pointer/source.ts b/packages/json-document/src/foundation/pointer/source.ts index 3163ce7d..5d9db5e9 100644 --- a/packages/json-document/src/foundation/pointer/source.ts +++ b/packages/json-document/src/foundation/pointer/source.ts @@ -1,4 +1,4 @@ -import { isPrefix, tryParsePointer, type Pointer } from "./index.js"; +import { isPrefix, tryParsePointer, type Pointer } from "./core.js"; export type PointerSource = Pointer | ReadonlyArray; diff --git a/packages/json-document/src/foundation/protocol/contract.ts b/packages/json-document/src/foundation/protocol/contract.ts index a6fa0b3e..5f3246c1 100644 --- a/packages/json-document/src/foundation/protocol/contract.ts +++ b/packages/json-document/src/foundation/protocol/contract.ts @@ -1,4 +1,4 @@ -import type { Pointer } from "../pointer/index.js"; +import type { Pointer } from "../pointer/core.js"; export type JSONValue = | null diff --git a/packages/json-document/src/foundation/protocol/index.ts b/packages/json-document/src/foundation/protocol/index.ts index b9d51be8..d8f096cd 100644 --- a/packages/json-document/src/foundation/protocol/index.ts +++ b/packages/json-document/src/foundation/protocol/index.ts @@ -1,5 +1,5 @@ import { trackPointer as trackPointerInternal } from "../patch/track.js"; -import type { Pointer } from "../pointer/index.js"; +import type { Pointer } from "../pointer/core.js"; import type { JSONPatchOperation } from "./contract.js"; export { @@ -37,8 +37,8 @@ export { parsePointer, readAt, tryParsePointer, -} from "../pointer/index.js"; -export type { Pointer } from "../pointer/index.js"; +} from "../pointer/core.js"; +export type { Pointer } from "../pointer/core.js"; export function trackPointer( pointer: Pointer, diff --git a/packages/json-document/tests/public/package-exports.test.ts b/packages/json-document/tests/public/package-exports.test.ts index e7c761f7..17a4d928 100644 --- a/packages/json-document/tests/public/package-exports.test.ts +++ b/packages/json-document/tests/public/package-exports.test.ts @@ -3,8 +3,6 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import * as rootApi from "@interactive-os/json-document"; -import * as reactApi from "@interactive-os/json-document/react"; -import * as sessionApi from "@interactive-os/json-document/session"; const packageRoot = resolve(__dirname, "..", ".."); const packageJson = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")) as { @@ -12,14 +10,10 @@ const packageJson = JSON.parse(readFileSync(resolve(packageRoot, "package.json") }; const publicContract = JSON.parse(readFileSync(resolve(packageRoot, "public-contract.json"), "utf8")) as { root: { values: string[]; types: string[] }; - session: { values: string[]; types: string[] }; - react: { values: string[]; types: string[] }; }; const publicEntrypoints = { root: { subpath: ".", runtime: rootApi }, - session: { subpath: "./session", runtime: sessionApi }, - react: { subpath: "./react", runtime: reactApi }, } as const; describe("package exports", () => { @@ -40,6 +34,8 @@ describe("package exports", () => { expect(Object.keys(packageJson.exports).sort()).toEqual( Object.values(publicEntrypoints).map(({ subpath }) => subpath).sort(), ); + expect(packageJson.exports).not.toHaveProperty("./session"); + expect(packageJson.exports).not.toHaveProperty("./react"); }); test("runtime value exports match public-contract.json", () => { diff --git a/packages/json-document/tests/smoke/package-smoke.mjs b/packages/json-document/tests/smoke/package-smoke.mjs index 44e80f8b..b90ab70c 100644 --- a/packages/json-document/tests/smoke/package-smoke.mjs +++ b/packages/json-document/tests/smoke/package-smoke.mjs @@ -1,38 +1,28 @@ import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; -import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; +import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { basename, dirname, isAbsolute, join, resolve } from "node:path"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); -const monorepoRoot = resolve(repoRoot, "..", ".."); -const workspace = await mkdtemp(join(tmpdir(), "json-document-package-")); +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const workspace = await mkdtemp(join(tmpdir(), "json-document-v2-package-")); const npmCache = join(workspace, ".npm-cache"); const npmEnv = { ...process.env, npm_config_cache: npmCache, - npm_config_dry_run: "false", npm_config_package_lock: "false", }; -const lockfilePath = join(monorepoRoot, "package-lock.json"); -const lockfileSnapshot = existsSync(lockfilePath) ? await readFile(lockfilePath) : null; -const packageJson = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf8")); -const publicContract = JSON.parse(await readFile(join(repoRoot, "public-contract.json"), "utf8")); -const readmeSource = await readFile(join(repoRoot, "README.md"), "utf8"); +const packageJson = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")); +const publicContract = JSON.parse( + await readFile(join(packageRoot, "public-contract.json"), "utf8"), +); const rootValueExports = publicContract.root.values; const rootTypeExports = publicContract.root.types; -const sessionValueExports = publicContract.session.values; -const sessionTypeExports = publicContract.session.types; -const reactValueExports = publicContract.react.values; -const reactTypeExports = publicContract.react.types; -const rootPublicExports = [...rootValueExports, ...rootTypeExports]; -const sessionPublicExports = [...sessionValueExports, ...sessionTypeExports]; -const reactPublicExports = [...reactValueExports, ...reactTypeExports]; -const rootTypeOnlyExports = [...rootTypeExports]; -const sessionTypeOnlyExports = [...sessionTypeExports]; -const reactTypeOnlyExports = [...reactTypeExports]; -const removedPublicSubpaths = [ +const rootExports = [...rootValueExports, ...rootTypeExports]; +const removedSubpaths = [ + "@interactive-os/json-document/session", + "@interactive-os/json-document/react", "@interactive-os/json-document/patch", "@interactive-os/json-document/pointer", "@interactive-os/json-document/selection", @@ -43,756 +33,136 @@ const removedPublicSubpaths = [ function run(command, args, cwd) { try { - execFileSync(command, args, { + return execFileSync(command, args, { cwd, env: npmEnv, - stdio: "pipe", + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], }); } catch (error) { - throw new Error(formatCommandFailure(command, args, cwd, error), { cause: error }); + const stdout = Buffer.isBuffer(error.stdout) + ? error.stdout.toString("utf8") + : String(error.stdout ?? ""); + const stderr = Buffer.isBuffer(error.stderr) + ? error.stderr.toString("utf8") + : String(error.stderr ?? ""); + throw new Error( + [ + `Command failed: ${[command, ...args].join(" ")}`, + `cwd: ${cwd}`, + stdout.trim() && `stdout:\n${stdout.trim()}`, + stderr.trim() && `stderr:\n${stderr.trim()}`, + ].filter(Boolean).join("\n\n"), + { cause: error }, + ); } } -function expectCommandFailure(command, args, cwd, expectedText) { +function expectImportFailure(specifier) { try { - execFileSync(command, args, { - cwd, - env: npmEnv, - stdio: "pipe", - }); + run( + "node", + ["--input-type=module", "--eval", `await import(${JSON.stringify(specifier)})`], + workspace, + ); } catch (error) { - const stdout = bufferToString(error.stdout); - const stderr = bufferToString(error.stderr); - const output = `${stdout}\n${stderr}`; - if (!output.includes(expectedText)) { - throw new Error(formatCommandFailure(command, args, cwd, error), { cause: error }); - } - return; + if (String(error).includes("ERR_PACKAGE_PATH_NOT_EXPORTED")) return; + throw error; } - - throw new Error(`Command unexpectedly succeeded in package smoke: ${[command, ...args].join(" ")}`); -} - -function formatCommandFailure(command, args, cwd, error) { - const output = []; - output.push(`Command failed in package smoke: ${[command, ...args].join(" ")}`); - output.push(`cwd: ${cwd}`); - const stdout = bufferToString(error.stdout); - const stderr = bufferToString(error.stderr); - if (stdout) output.push(`stdout:\n${stdout}`); - if (stderr) output.push(`stderr:\n${stderr}`); - return output.join("\n\n"); -} - -function bufferToString(value) { - if (Buffer.isBuffer(value)) return value.toString("utf8").trim(); - if (typeof value === "string") return value.trim(); - return ""; -} - -function existingZodPackage() { - return existingPath([ - join(repoRoot, "node_modules", "zod"), - join(repoRoot, "..", "..", "node_modules", "zod"), - ]); -} - -function existingTypeScriptBin() { - return existingPath([ - join(repoRoot, "node_modules", "typescript", "bin", "tsc"), - join(repoRoot, "..", "..", "node_modules", "typescript", "bin", "tsc"), - ]); -} - -function existingReactPackage() { - return existingPath([ - join(repoRoot, "node_modules", "react"), - join(repoRoot, "..", "..", "node_modules", "react"), - ]); -} - -function existingReactTypesPackage() { - return existingPath([ - join(repoRoot, "node_modules", "@types", "react"), - join(repoRoot, "..", "..", "node_modules", "@types", "react"), - ]); + throw new Error(`Private package path unexpectedly resolved: ${specifier}`); } -function existingPath(candidates) { - for (const candidate of candidates) { - if (existsSync(candidate)) { - return candidate; +async function filesUnder(root, base = root) { + const result = []; + for (const entry of await readdir(root, { withFileTypes: true })) { + const path = join(root, entry.name); + if (entry.isDirectory()) { + result.push(...await filesUnder(path, base)); + } else if (entry.isFile()) { + result.push(relative(base, path)); } } - - return null; + return result.sort(); } -function markdownCodeBlockAfterHeading(source, heading, language) { - const rest = markdownSection(source, heading); - const match = rest.match(new RegExp(`\`\`\`${language}\\n([\\s\\S]*?)\\n\`\`\``)); - if (!match?.[1]) throw new Error(`README ${heading} ${language} code block missing`); - return match[1]; -} - -function markdownSection(source, heading) { - const headingIndex = source.indexOf(`## ${heading}`); - if (headingIndex === -1) throw new Error(`README heading missing: ${heading}`); - const rest = source.slice(headingIndex); - const nextHeadingIndex = rest.slice(1).search(/\n## /); - return nextHeadingIndex === -1 ? rest : rest.slice(0, nextHeadingIndex + 1); -} - -function markdownCodeBlocksAfterHeading(source, heading, language) { - const section = markdownSection(source, heading); - const blocks = Array.from( - section.matchAll(new RegExp(`\`\`\`${language}\\n([\\s\\S]*?)\\n\`\`\``, "g")), - (match) => { - const block = match[1]; - if (block === undefined) throw new Error(`README ${heading} ${language} code block capture failed`); - return block; - }, - ); - if (blocks.length === 0) throw new Error(`README ${heading} has no ${language} code blocks`); - return blocks; -} - -function assertDeclarationExports(declarationSource, expectedNames, label) { - const actualNames = declarationExportNames(declarationSource).sort(); - const sortedExpectedNames = [...expectedNames].sort(); - if (JSON.stringify(actualNames) !== JSON.stringify(sortedExpectedNames)) { - throw new Error( - `${label} declaration exports mismatch:\nexpected ${sortedExpectedNames.join(", ")}\nactual ${actualNames.join(", ")}`, - ); - } - - for (const name of expectedNames) { - const exportNamePattern = new RegExp(`(^|[^A-Za-z0-9_$])${name}([^A-Za-z0-9_$]|$)`); - if (!exportNamePattern.test(declarationSource)) { - throw new Error(`${label} declaration export missing: ${name}`); - } - } +function moduleSpecifiers(source) { + return [ + ...source.matchAll(/\b(?:import|export)\s+(?:type\s+)?[^;"']*?\s+from\s+["']([^"']+)["']/g), + ...source.matchAll(/\bimport\s+["']([^"']+)["']/g), + ].map((match) => match[1]); } -function declarationExportNames(declarationSource) { +function declarationExportNames(source) { const names = []; - - for (const match of declarationSource.matchAll(/\bexport\s+(?:type\s+)?\{([^}]*)\}/g)) { - const list = match[1]; - if (list === undefined) throw new Error("Declaration export list capture failed"); - names.push(...exportListNames(list)); - } - - for (const match of declarationSource.matchAll(/\bexport\s+declare\s+(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g)) { - const name = match[1]; - if (name === undefined) throw new Error("Declaration export declaration capture failed"); - names.push(name); + for (const match of source.matchAll(/\bexport\s+(?:type\s+)?\{([^}]*)\}/g)) { + names.push( + ...match[1] + .split(",") + .map((part) => part.trim()) + .filter(Boolean) + .map((part) => { + const normalized = part.replace(/^type\s+/, ""); + return normalized.match(/\s+as\s+([A-Za-z_$][\w$]*)$/)?.[1] + ?? normalized.split(/\s+/)[0]; + }), + ); } - return [...new Set(names)].sort(); } -function exportListNames(list) { - return list - .split(",") - .map((part) => part.trim()) - .filter((part) => part.length > 0) - .map((part) => { - const withoutType = part.replace(/^type\s+/, ""); - const aliasMatch = /\s+as\s+([A-Za-z_$][\w$]*)$/.exec(withoutType); - if (aliasMatch?.[1]) return aliasMatch[1]; - const [name] = withoutType.split(/\s+/); - if (!name) throw new Error(`Declaration export name parse failed: ${part}`); - return name; - }); -} - -async function assertDeclarationSpecifiers(installedPackageRoot) { - const distRoot = join(installedPackageRoot, "dist"); - const declarationPaths = await declarationModulePaths(distRoot); - const declarationSet = new Set(declarationPaths); - const allowedBareSpecifiers = new Set(["react", "zod"]); - - for (const declarationPath of declarationPaths) { - const source = await readFile(join(distRoot, declarationPath), "utf8"); - const specifiers = moduleSpecifiers(source); - - for (const specifier of specifiers) { - if (specifier === undefined) throw new Error(`Declaration specifier capture failed: ${declarationPath}`); - if (specifier.startsWith(".")) { - const resolved = resolve(dirname(join(distRoot, declarationPath)), specifier) - .replace(/\.js$/, ".d.ts") - .slice(distRoot.length + 1); - if (!declarationSet.has(resolved)) { - throw new Error(`Declaration import does not resolve inside package: ${declarationPath} -> ${specifier}`); - } - } else if (!allowedBareSpecifiers.has(specifier)) { - throw new Error(`Declaration imports unexpected bare specifier: ${declarationPath} -> ${specifier}`); - } - } +try { + if (Object.keys(packageJson.exports).join(",") !== ".") { + throw new Error("The published package must expose only the root entrypoint."); } -} - -async function assertRuntimeSpecifiers(installedPackageRoot) { - const distRoot = join(installedPackageRoot, "dist"); - const runtimePaths = await runtimeModulePaths(distRoot); - const runtimeSet = new Set(runtimePaths); - const allowedBareSpecifiers = new Set(["react", "zod"]); - - for (const runtimePath of runtimePaths) { - const source = await readFile(join(distRoot, runtimePath), "utf8"); - const specifiers = moduleSpecifiers(source); - - for (const specifier of specifiers) { - if (specifier === undefined) throw new Error(`Runtime specifier capture failed: ${runtimePath}`); - if (specifier.startsWith(".")) { - const resolved = resolve(dirname(join(distRoot, runtimePath)), specifier) - .slice(distRoot.length + 1); - if (!runtimeSet.has(resolved)) { - throw new Error(`Runtime import does not resolve inside package: ${runtimePath} -> ${specifier}`); - } - } else if (!allowedBareSpecifiers.has(specifier)) { - throw new Error(`Runtime imports unexpected bare specifier: ${runtimePath} -> ${specifier}`); - } - } + if (packageJson.peerDependencies !== undefined) { + throw new Error("The v2 kernel must not publish peer dependencies."); } -} - -function moduleSpecifiers(source) { - return [ - ...Array.from(source.matchAll(/\b(?:import|export)\s+[^;"']*\s+from\s+["']([^"']+)["']/g), (match) => match[1]), - ...Array.from(source.matchAll(/\bimport\s+["']([^"']+)["']/g), (match) => match[1]), - ]; -} - -async function declarationModulePaths(dir, prefix = "") { - const entries = await readdir(dir, { withFileTypes: true }); - const paths = []; - for (const entry of entries) { - const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; - const absolutePath = join(dir, entry.name); - if (entry.isDirectory()) { - paths.push(...await declarationModulePaths(absolutePath, relativePath)); - } else if (entry.isFile() && entry.name.endsWith(".d.ts")) { - paths.push(relativePath); - } + if (packageJson.peerDependenciesMeta !== undefined) { + throw new Error("The v2 kernel must not publish peer dependency metadata."); } - - return paths.sort(); -} - -async function runtimeModulePaths(dir, prefix = "") { - const entries = await readdir(dir, { withFileTypes: true }); - const paths = []; - for (const entry of entries) { - const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; - const absolutePath = join(dir, entry.name); - if (entry.isDirectory()) { - paths.push(...await runtimeModulePaths(absolutePath, relativePath)); - } else if (entry.isFile() && entry.name.endsWith(".js")) { - paths.push(relativePath); - } + if (packageJson.dependencies !== undefined) { + throw new Error("The v2 kernel must not publish runtime dependencies."); } - - return paths.sort(); -} - -function assertInstalledPackageJson(pkg) { - const expectedFields = [ - "name", - "version", - "description", - "type", - "license", - "sideEffects", - "main", - "types", - "homepage", - "repository", - "bugs", - "publishConfig", - "keywords", - "exports", - "peerDependencies", - "peerDependenciesMeta", - ]; - for (const field of expectedFields) { - if (JSON.stringify(pkg[field]) !== JSON.stringify(packageJson[field])) { - throw new Error(`Installed package.json field mismatch: ${field}`); - } - } - const forbiddenFields = [ - "bin", - "config", - "dependencies", - "optionalDependencies", - "overrides", - "private", - "workspaces", - ]; - for (const field of forbiddenFields) { - if (pkg[field] !== undefined) { - throw new Error(`Installed package.json must not include ${field}`); - } - } - const installLifecycleScripts = ["preinstall", "install", "postinstall", "prepare"]; - for (const script of installLifecycleScripts) { - if (pkg.scripts?.[script] !== undefined) { - throw new Error(`Installed package.json must not include install lifecycle script: ${script}`); - } + if (rootValueExports.length !== 8 || rootTypeExports.length !== 12) { + throw new Error("The root contract must contain exactly 8 values and 12 types."); } -} -function namedImportLine(names, specifier, options = {}) { - if (names.length === 0) return null; - const { prefix = "", typeOnly = false } = options; - const keyword = typeOnly ? "import type" : "import"; - const imports = names - .map((name) => prefix ? `${name} as ${prefix}${name}` : name) - .join(", "); - return `${keyword} { ${imports} } from "${specifier}";`; -} - -async function assertInstalledTextFiles(installedPackageRoot) { - const files = packageJson.files.filter((file) => file !== "dist"); - for (const file of files) { - const source = await readFile(join(repoRoot, file), "utf8"); - const installed = await readFile(join(installedPackageRoot, file), "utf8"); - if (installed !== source) { - throw new Error(`Installed package text file differs from source: ${file}`); - } - } -} - -try { - const packOutput = execFileSync( + const packResult = JSON.parse(run( "npm", ["pack", "--json", "--pack-destination", workspace], - { - cwd: repoRoot, - encoding: "utf8", - env: npmEnv, - stdio: ["ignore", "pipe", "pipe"], - }, - ); - const [packResult] = JSON.parse(packOutput); + packageRoot, + ))[0]; const tarball = isAbsolute(packResult.filename) ? packResult.filename : join(workspace, packResult.filename); - const zodPackage = existingZodPackage(); - const typeScriptBin = existingTypeScriptBin(); - const reactPackage = existingReactPackage(); - const reactTypesPackage = existingReactTypesPackage(); - - if (packResult.name !== packageJson.name) { - throw new Error(`Packed package name mismatch: ${packResult.name}`); - } - if (packResult.version !== packageJson.version) { - throw new Error(`Packed package version mismatch: ${packResult.version}`); - } - const expectedTarballName = `${packageJson.name.replace(/^@/, "").replace("/", "-")}-${packageJson.version}.tgz`; - if (packResult.filename !== expectedTarballName) { - throw new Error(`Packed tarball filename mismatch: ${packResult.filename}`); - } - if (typeof packResult.integrity !== "string" || !packResult.integrity.startsWith("sha512-")) { - throw new Error(`Packed tarball must include sha512 integrity: ${packResult.integrity}`); - } - if (typeof packResult.size !== "number" || packResult.size <= 0) { - throw new Error(`Packed tarball must report a positive compressed size: ${packResult.size}`); - } - if (typeof packResult.unpackedSize !== "number" || packResult.unpackedSize <= packResult.size) { - throw new Error(`Packed tarball must report an unpacked size larger than compressed size: ${packResult.unpackedSize}`); - } - - if (!existsSync(tarball)) { - throw new Error(`Packed tarball was not created: ${tarball}`); - } - - // #62 guard — published tarball 에 node_modules 가 포함되면 소비자 측 zod type 중복으로 generic 추론이 깨진다. - if (!Array.isArray(packResult.files) || packResult.files.length === 0) { - throw new Error("Packed tarball must report a non-empty files list"); + if (!existsSync(tarball)) throw new Error(`Tarball missing: ${tarball}`); + if (packResult.name !== packageJson.name || packResult.version !== packageJson.version) { + throw new Error("Packed identity does not match package.json."); } if (packResult.entryCount !== packResult.files.length) { - throw new Error(`Packed tarball entryCount mismatch: ${packResult.entryCount} !== ${packResult.files.length}`); - } - if (!Array.isArray(packResult.bundled) || packResult.bundled.length !== 0) { - throw new Error(`Packed tarball must not bundle dependencies: ${JSON.stringify(packResult.bundled)}`); + throw new Error("Packed file count does not match npm metadata."); } - const reportedUnpackedSize = packResult.files.reduce((total, file) => { - if (typeof file.path !== "string" || file.path.length === 0) { - throw new Error(`Packed file entry must include a path: ${JSON.stringify(file)}`); - } - if (typeof file.size !== "number" || file.size <= 0) { - throw new Error(`Packed file must report a positive size: ${file.path}`); - } - if (file.mode !== 0o644) { - throw new Error(`Packed file must use regular read/write file mode 0644: ${file.path}`); - } - return total + file.size; - }, 0); - if (reportedUnpackedSize !== packResult.unpackedSize) { - throw new Error(`Packed tarball unpackedSize mismatch: ${reportedUnpackedSize} !== ${packResult.unpackedSize}`); - } - const packedFiles = packResult.files.map((f) => f.path); - const offenders = packedFiles.filter((p) => p.includes("node_modules/")); - if (offenders.length > 0) { - throw new Error(`Tarball must not include node_modules: ${offenders.slice(0, 3).join(", ")}`); - } - const requiredPackageFiles = packageJson.files.filter((file) => file !== "dist"); - requiredPackageFiles.push("package.json"); - for (const required of requiredPackageFiles) { - if (!packedFiles.includes(required)) { - throw new Error(`Tarball is missing required package file: ${required}`); - } - } - const allowedPackedRoots = new Set([...packageJson.files, "package.json"]); - const unexpectedPackedFiles = packedFiles.filter((file) => { - const [root] = file.split("/"); - return !allowedPackedRoots.has(root); - }); - if (unexpectedPackedFiles.length > 0) { - throw new Error(`Tarball includes unexpected files: ${unexpectedPackedFiles.slice(0, 3).join(", ")}`); - } - const developmentArtifacts = packedFiles.filter((file) => { - const name = basename(file); - if (file.endsWith(".d.ts")) return false; - return ( - file.endsWith(".ts") || - file.endsWith(".tsx") || - file.endsWith(".map") || - name.endsWith(".test.js") || - name.endsWith(".test.ts") || - name.endsWith(".test-d.ts") || - name === "tsconfig.json" || - name.startsWith("tsconfig.") || - name === "vitest.config.ts" - ); - }); - if (developmentArtifacts.length > 0) { - throw new Error(`Tarball includes development artifacts: ${developmentArtifacts.slice(0, 3).join(", ")}`); + if (packResult.bundled.length !== 0) { + throw new Error("The v2 kernel must not bundle dependencies."); } - if (packageJson.type !== "module") { - throw new Error('Package must publish as ESM with "type": "module"'); - } - if (packageJson.sideEffects !== false) { - throw new Error('Package must declare "sideEffects": false'); - } - if (packageJson.dependencies !== undefined && Object.keys(packageJson.dependencies).length > 0) { - throw new Error(`Package must not publish runtime dependencies: ${Object.keys(packageJson.dependencies).join(",")}`); - } - const expectedPeers = { react: ">=18", zod: "^4.0.0" }; - if (JSON.stringify(packageJson.peerDependencies) !== JSON.stringify(expectedPeers)) { - throw new Error( - `Package peerDependencies must be ${JSON.stringify(expectedPeers)}: ${JSON.stringify(packageJson.peerDependencies)}`, - ); - } - if (packageJson.peerDependenciesMeta?.react?.optional !== true) { - throw new Error("React peer dependency must be optional"); - } - if (packageJson.peerDependenciesMeta?.zod?.optional !== true) { - throw new Error("Zod peer dependency must be optional"); - } - const unexpectedPeerMeta = Object.keys(packageJson.peerDependenciesMeta ?? {}) - .filter((name) => name !== "react" && name !== "zod"); - if (unexpectedPeerMeta.length > 0) { - throw new Error(`Package has unexpected peer dependency metadata: ${unexpectedPeerMeta.join(",")}`); - } - if (packageJson.main !== packageJson.exports["."].import) { - throw new Error(`Package main must match root import export: ${packageJson.main}`); - } - if (packageJson.types !== packageJson.exports["."].types) { - throw new Error(`Package types must match root types export: ${packageJson.types}`); - } - for (const [subpath, exportMap] of Object.entries(packageJson.exports)) { - const conditions = Object.keys(exportMap); - const expectedConditions = ["types", "import"]; - if (JSON.stringify(conditions) !== JSON.stringify(expectedConditions)) { - throw new Error(`Export ${subpath} conditions must be ${expectedConditions.join(",")}: ${conditions.join(",")}`); + + const packedFiles = packResult.files.map((file) => file.path); + const allowedRoots = new Set([...packageJson.files, "package.json"]); + for (const file of packedFiles) { + if (!allowedRoots.has(file.split("/")[0])) { + throw new Error(`Unexpected packed file: ${file}`); } - for (const condition of expectedConditions) { - const target = exportMap[condition]; - const packedPath = target.replace(/^\.\//, ""); - if (!packedFiles.includes(packedPath)) { - throw new Error(`Export ${subpath}.${condition} target is missing from tarball: ${target}`); - } + if ( + file.includes("node_modules/") + || file.endsWith(".map") + || (file.endsWith(".ts") && !file.endsWith(".d.ts")) + ) { + throw new Error(`Development artifact was packed: ${file}`); } } - if (zodPackage === null) { - throw new Error("Local zod dependency is missing. Run npm install first."); - } - - if (typeScriptBin === null) { - throw new Error("Local TypeScript dependency is missing. Run npm install first."); - } - - if (reactPackage === null) { - throw new Error("Local react dependency is missing. Run npm install first."); - } - - if (reactTypesPackage === null) { - throw new Error("Local @types/react dependency is missing. Run npm install first."); - } - - await writeFile( - join(workspace, "package.json"), - JSON.stringify({ private: true }, null, 2), - ); - await writeFile( - join(workspace, "root-smoke.mjs"), - [ - 'import * as core from "@interactive-os/json-document";', - 'import { applyPatch, createJSONDocument } from "@interactive-os/json-document";', - `const expectedRootValueExports = ${JSON.stringify(rootValueExports)};`, - `const expectedRootTypeOnlyExports = ${JSON.stringify(rootTypeOnlyExports)};`, - 'const actualRootValueExports = Object.keys(core).sort();', - 'if (JSON.stringify(actualRootValueExports) !== JSON.stringify([...expectedRootValueExports].sort())) {', - ' throw new Error(`root runtime exports mismatch: ${actualRootValueExports.join(", ")}`);', - '}', - 'for (const name of expectedRootTypeOnlyExports) {', - ' if (name in core) throw new Error(`${name} type-only root export leaked at runtime`);', - '}', - 'const initial = { title: "draft", tags: [] };', - 'const patched = applyPatch(initial, [{ op: "add", path: "/tags/-", value: "core" }]);', - 'if (!patched.ok || patched.value.tags[0] !== "core") throw new Error("root applyPatch failed");', - 'if (patched.change.applied[0]?.path !== "/tags/0") throw new Error("root canonical patch failed");', - 'const document = createJSONDocument(initial);', - 'const changes = [];', - 'document.subscribe((change) => changes.push(change));', - 'const committed = document.commit([{ op: "replace", path: "/title", value: "ready" }]);', - 'if (!committed.ok || document.value.title !== "ready") throw new Error("root commit failed");', - 'if (changes.length !== 1) throw new Error("root publication failed");', - ].join("\n"), - ); - await writeFile( - join(workspace, "session-smoke.mjs"), - [ - 'import * as z from "zod";', - 'import * as zc from "@interactive-os/json-document/session";', - 'import { applyOperation, applyPatch, applyPatchToTrustedState, createJSONDocument, parsePointer, tryParsePointer, buildPointer, parentPointer, lastSegment, lastSegmentIndex, appendSegment, withLastSegment } from "@interactive-os/json-document/session";', - `const expectedSessionValueExports = ${JSON.stringify(sessionValueExports)};`, - `const expectedSessionTypeOnlyExports = ${JSON.stringify(sessionTypeOnlyExports)};`, - 'const actualSessionValueExports = Object.keys(zc).sort();', - 'if (JSON.stringify(actualSessionValueExports) !== JSON.stringify([...expectedSessionValueExports].sort())) {', - ' throw new Error(`session runtime exports mismatch: ${actualSessionValueExports.join(", ")}`);', - '}', - 'for (const name of expectedSessionValueExports) {', - ' if (!(name in zc)) throw new Error(`${name} session runtime export missing`);', - '}', - 'for (const name of expectedSessionTypeOnlyExports) {', - ' if (name in zc) throw new Error(`${name} type-only root export leaked at runtime`);', - '}', - 'const schema = z.object({ name: z.string(), tags: z.array(z.string()) });', - 'const initial = { name: "ok", tags: [] };', - 'const r = applyOperation(schema, initial, { op: "replace", path: "/name", value: "next" });', - 'if (!r.result.ok) throw new Error("applyOperation failed");', - 'if (r.state.name !== "next") throw new Error("state mismatch");', - 'const r2 = applyPatch(schema, initial, [', - ' { op: "add", path: "/tags/-", value: "a" },', - ' { op: "replace", path: "/name", value: "x" },', - ']);', - 'if (!r2.result.ok) throw new Error("applyPatch failed");', - 'if (r2.state.tags.length !== 1) throw new Error("batch tags failed");', - 'const r3 = applyPatchToTrustedState(schema, initial, [{ op: "replace", path: "/name", value: "trusted" }]);', - 'if (!r3.result.ok || r3.state.name !== "trusted") throw new Error("applyPatchToTrustedState failed");', - 'if (parsePointer("/a/0").length !== 2) throw new Error("parsePointer failed");', - 'if (tryParsePointer("/a/0")?.length !== 2) throw new Error("tryParsePointer valid failed");', - 'if (tryParsePointer("a/0") !== null) throw new Error("tryParsePointer invalid failed");', - 'if (buildPointer(["a", 0]) !== "/a/0") throw new Error("buildPointer failed");', - 'if (parentPointer("/a/0") !== "/a") throw new Error("parentPointer failed");', - 'if (lastSegment("/a/0") !== "0") throw new Error("lastSegment failed");', - 'if (lastSegmentIndex("/a/0") !== 0) throw new Error("lastSegmentIndex failed");', - 'if (appendSegment("/a", "b/c") !== "/a/b~1c") throw new Error("appendSegment failed");', - 'if (withLastSegment("/a/0", 1) !== "/a/1") throw new Error("withLastSegment failed");', - 'if (typeof createJSONDocument !== "function") throw new Error("createJSONDocument export failed");', - 'const jsonDoc = createJSONDocument(schema, initial);', - 'const jsonPatch = jsonDoc.patch({ op: "replace", path: "/name", value: "json" });', - 'if (!jsonPatch.ok || jsonDoc.value.name !== "json") throw new Error("createJSONDocument runtime failed");', - 'if (!jsonDoc.at("/name").ok) throw new Error("createJSONDocument read facade failed");', - 'const BoardSchema = z.object({ lists: z.array(z.object({ cards: z.array(z.object({ id: z.string(), title: z.string(), done: z.boolean() })) })) });', - 'const boardDoc = createJSONDocument(BoardSchema, { lists: [{ cards: [{ id: "a", title: "A", done: false }, { id: "b", title: "B", done: false }] }] }, { history: 10, selection: { mode: "extended", initial: ["/lists/0/cards/0"] } });', - 'const foundCards = boardDoc.find("$.lists[*].cards[*]");', - 'if (!foundCards.ok || foundCards.pointers.length !== 2) throw new Error("public interface query failed");', - 'if (!boardDoc.canInsert("/lists/0/cards/-", { id: "c", title: "C", done: false }).ok) throw new Error("public interface canInsert failed");', - 'const insertedCard = boardDoc.insert("/lists/0/cards/-", { id: "c", title: "C", done: false });', - 'if (!insertedCard.ok || boardDoc.value.lists[0]?.cards.length !== 3) throw new Error("public interface insert failed");', - 'boardDoc.selection?.selectRanges(foundCards.pointers);', - 'if (boardDoc.selection?.selectedPointers.length !== 2) throw new Error("public interface selection failed");', - 'if (!boardDoc.canCopy(boardDoc.selection?.selectedPointers ?? []).ok) throw new Error("public interface canCopy failed");', - 'const copiedCards = boardDoc.copy(boardDoc.selection?.selectedPointers ?? []);', - 'if (!copiedCards.ok || !boardDoc.clipboard.hasData) throw new Error("public interface clipboard copy failed");', - 'const pastedCards = boardDoc.paste("/lists/0/cards/-", { spread: true, rekey: { fields: ["id"], strategy: "suffix" } });', - 'if (!pastedCards.ok || boardDoc.value.lists[0]?.cards.length !== 5) throw new Error("public interface clipboard paste failed");', - 'const duplicatedCard = boardDoc.duplicate("/lists/0/cards/0", { rekey: { fields: ["id"], strategy: "suffix" } });', - 'if (!duplicatedCard.ok || boardDoc.value.lists[0]?.cards.length !== 6) throw new Error("public interface duplicate failed");', - 'if (!boardDoc.canDelete("/lists/0/cards/0").ok) throw new Error("public interface canDelete failed");', - 'const deletedCard = boardDoc.delete("/lists/0/cards/0");', - 'if (!deletedCard.ok || boardDoc.value.lists[0]?.cards.length !== 5) throw new Error("public interface delete failed");', - 'if (!boardDoc.canUndo().ok || !boardDoc.undo()) throw new Error("public interface history failed");', - ].join("\n"), - ); - await writeFile( - join(workspace, "smoke.ts"), - [ - 'import * as z from "zod";', - 'import { applyOperation, applyPatch, applyPatchToTrustedState, tryParsePointer, parentPointer, lastSegment, lastSegmentIndex, appendSegment, withLastSegment, type JSONPatchOperation, type Pointer } from "@interactive-os/json-document/session";', - 'import type { HistoryTransactionOptions, JSONCapabilityResult, JSONChangeMetadata, JSONDocument, JSONDocumentCommitOptions, JSONDocumentDuplicateError, JSONDocumentDuplicateOptions, JSONDocumentDuplicateResult, JSONDocumentEditError, JSONDocumentEditResult, JSONDocumentHistory, JSONDocumentInsertOptions, JSONDocumentInsertTarget, JSONDocumentMoveTarget, JSONDocumentOptions, JSONDocumentPasteOptions, JSONDocumentPasteTarget, JSONDocumentSelectionTarget, JSONPatchInput, SelectionPoint, JSONResult } from "@interactive-os/json-document/session";', - 'import type { ClipboardCopyError, ClipboardCopyOk, ClipboardCopyOptions, ClipboardCopyResult, ClipboardCutError, ClipboardCutOk, ClipboardCutOptions, ClipboardCutResult, ClipboardEmpty, ClipboardMutationOk, ClipboardPasteDiscriminatorMismatch, ClipboardPasteError, ClipboardPasteResult, ClipboardReadOk, ClipboardReadOptions, ClipboardReadResult, ClipboardState, ClipboardWriteOptions, ClipboardSource } from "@interactive-os/json-document/session";', - 'import type { EntriesResult, EntryKind, QueryResult, ReadEntry, ReadResult, SchemaDescription, SchemaDescriptionResult, SchemaErrorCode, SchemaErrorResult, SchemaKind, SchemaKindResult, SchemaPathMode, SchemaQueryResult, SchemaState } from "@interactive-os/json-document/session";', - 'import type { DeleteSelectionTextResult, SelectionPointObject, SelectionOrderedRange, SelectionOrderedRangeEntry, ReplaceSelectionTextResult, SelectionAffinity, SelectionContext, SelectionCursorDirection, SelectionCursorErrorCode, SelectionCursorOptions, SelectionCursorResult, SelectionCursorTarget, SelectionDirection, SelectionEdge, SelectionMode, SelectionOptions, SelectionOrderErrorCode, SelectionOrderOptions, SelectionPointOrderResult, SelectionPointerSpan, SelectionPointerSpansResult, SelectionRange, SelectionRangeInput, SelectionRangeOrderResult, SelectionRangesOrderResult, SelectionScopeErrorCode, SelectionScopeOptions, SelectionScopeResult, SelectionScopeTarget, SelectionSource, SelectionSpanOptions, SelectionSnap, SelectionState, SelectionTextDeleteDirection, SelectionTextDeleteOptions, SelectionTextEdit, SelectionTextEditErrorCode, SelectionTextEditOptions, SelectionTextEditsResult, SelectionType } from "@interactive-os/json-document/session";', - 'const schema = z.object({ name: z.string() });', - 'type Row = z.output;', - 'type PublicRootTypes = [HistoryTransactionOptions, JSONCapabilityResult, JSONChangeMetadata, JSONDocument, JSONDocumentCommitOptions, JSONDocumentDuplicateError, JSONDocumentDuplicateOptions, JSONDocumentDuplicateResult, JSONDocumentEditError, JSONDocumentEditResult, JSONDocumentHistory, JSONDocumentInsertOptions, JSONDocumentInsertTarget, JSONDocumentMoveTarget, JSONDocumentOptions, JSONDocumentPasteOptions, JSONDocumentPasteTarget, JSONDocumentSelectionTarget, JSONPatchInput, JSONPatchOperation, SelectionPoint, JSONResult, Pointer, ClipboardCopyError, ClipboardCopyOk, ClipboardCopyOptions, ClipboardCopyResult, ClipboardCutError, ClipboardCutOk, ClipboardCutOptions, ClipboardCutResult, ClipboardEmpty, ClipboardMutationOk, ClipboardPasteDiscriminatorMismatch, ClipboardPasteError, ClipboardPasteResult, ClipboardReadOk, ClipboardReadOptions, ClipboardReadResult, ClipboardState, ClipboardWriteOptions, ClipboardSource, EntriesResult, EntryKind, QueryResult, ReadEntry, ReadResult, SchemaDescription, SchemaDescriptionResult, SchemaErrorCode, SchemaErrorResult, SchemaKind, SchemaKindResult, SchemaPathMode, SchemaQueryResult, SchemaState, DeleteSelectionTextResult, SelectionPointObject, SelectionOrderedRange, SelectionOrderedRangeEntry, ReplaceSelectionTextResult, SelectionAffinity, SelectionContext, SelectionCursorDirection, SelectionCursorErrorCode, SelectionCursorOptions, SelectionCursorResult, SelectionCursorTarget, SelectionDirection, SelectionEdge, SelectionMode, SelectionOptions, SelectionOrderErrorCode, SelectionOrderOptions, SelectionPointOrderResult, SelectionPointerSpan, SelectionPointerSpansResult, SelectionRange, SelectionRangeInput, SelectionRangeOrderResult, SelectionRangesOrderResult, SelectionScopeErrorCode, SelectionScopeOptions, SelectionScopeResult, SelectionScopeTarget, SelectionSource, SelectionSpanOptions, SelectionSnap, SelectionState, SelectionTextDeleteDirection, SelectionTextDeleteOptions, SelectionTextEdit, SelectionTextEditErrorCode, SelectionTextEditOptions, SelectionTextEditsResult, SelectionType];', - 'declare const publicRootTypes: PublicRootTypes;', - 'publicRootTypes satisfies readonly unknown[];', - 'const r = applyOperation(schema, { name: "ok" }, { op: "replace", path: "/name", value: "next" });', - 'r.state.name satisfies string;', - 'const ops: JSONPatchOperation[] = [{ op: "replace", path: "/name", value: "y" }];', - 'const r2 = applyPatch(schema, { name: "ok" }, ops);', - 'r2.state.name satisfies string;', - 'const r3 = applyPatchToTrustedState(schema, { name: "ok" }, ops);', - 'r3.state.name satisfies string;', - 'const _selection = null as unknown as SelectionState;', - '_selection.subscribe((_snapshot, _previous) => undefined) satisfies () => void;', - '_selection.togglePointer("/name") satisfies void;', - '_selection.selectionRanges satisfies readonly SelectionRange[];', - '_selection.type satisfies "None" | "Caret" | "Range";', - 'const pasteTargetPointer: JSONDocumentPasteTarget = "/items/-";', - 'const pasteTargetAfter: JSONDocumentPasteTarget = { after: "/items/0" };', - 'const pasteTargetBefore: JSONDocumentPasteTarget = { before: "/items/0" };', - 'const pasteTargetReplace: JSONDocumentPasteTarget = { replace: "/items/0" };', - 'const insertTargetInto: JSONDocumentInsertTarget = { into: "/items" };', - 'const moveTargetAfter: JSONDocumentMoveTarget = { after: "/items/0" };', - 'pasteTargetPointer satisfies string;', - 'pasteTargetAfter satisfies { after: string };', - 'pasteTargetBefore satisfies { before: string };', - 'pasteTargetReplace satisfies { replace: string };', - 'insertTargetInto satisfies { into: string };', - 'moveTargetAfter satisfies { after: string };', - '// @ts-expect-error { at } is intentionally not a public paste target; pass the insertion pointer directly.', - 'const pasteTargetAt: JSONDocumentPasteTarget = { at: "/items/-" };', - 'const p: Pointer = "/name";', - 'p satisfies string;', - 'const parsedPointer = tryParsePointer(p);', - 'parsedPointer satisfies string[] | null;', - 'parentPointer(p) satisfies string | null;', - 'lastSegment(p) satisfies string | null;', - 'lastSegmentIndex(p) satisfies number | null;', - 'appendSegment(p, "next") satisfies string;', - 'withLastSegment(p, "other") satisfies string | null;', - ].join("\n"), - ); - await writeFile( - join(workspace, "subpath-smoke.mjs"), - [ - `const removedPublicSubpaths = ${JSON.stringify(removedPublicSubpaths)};`, - 'for (const specifier of removedPublicSubpaths) {', - ' try {', - ' await import(specifier);', - ' } catch (error) {', - ' const output = `${error?.code ?? ""}\\n${error?.message ?? ""}`;', - ' if (!output.includes("ERR_PACKAGE_PATH_NOT_EXPORTED")) throw error;', - ' continue;', - ' }', - ' throw new Error(`${specifier} unexpectedly resolved`);', - '}', - ].join("\n"), - ); - await writeFile( - join(workspace, "named-imports-smoke.ts"), - [ - namedImportLine(rootValueExports, "@interactive-os/json-document", { prefix: "RootValue_" }), - namedImportLine(rootTypeExports, "@interactive-os/json-document", { prefix: "RootType_", typeOnly: true }), - namedImportLine(sessionValueExports, "@interactive-os/json-document/session", { prefix: "SessionValue_" }), - namedImportLine(sessionTypeExports, "@interactive-os/json-document/session", { prefix: "SessionType_", typeOnly: true }), - namedImportLine(reactValueExports, "@interactive-os/json-document/react", { prefix: "ReactValue_" }), - namedImportLine(reactTypeExports, "@interactive-os/json-document/react", { prefix: "ReactType_", typeOnly: true }), - "const rootValues = {", - ...rootValueExports.map((name) => ` ${name}: RootValue_${name},`), - "};", - "const sessionValues = {", - ...sessionValueExports.map((name) => ` ${name}: SessionValue_${name},`), - "};", - "const reactValues = {", - ...reactValueExports.map((name) => ` ${name}: ReactValue_${name},`), - "};", - "rootValues satisfies Record;", - "sessionValues satisfies Record;", - "reactValues satisfies Record;", - ].filter((line) => line !== null).join("\n"), - ); - await writeFile( - join(workspace, "react-smoke.mjs"), - [ - 'import * as zcr from "@interactive-os/json-document/react";', - 'import { useJSONDocument } from "@interactive-os/json-document/react";', - `const expectedReactValueExports = ${JSON.stringify(reactValueExports)};`, - `const expectedReactTypeOnlyExports = ${JSON.stringify(reactTypeOnlyExports)};`, - 'const actualReactValueExports = Object.keys(zcr).sort();', - 'if (JSON.stringify(actualReactValueExports) !== JSON.stringify([...expectedReactValueExports].sort())) {', - ' throw new Error(`react runtime exports mismatch: ${actualReactValueExports.join(", ")}`);', - '}', - 'for (const name of expectedReactValueExports) {', - ' if (!(name in zcr)) throw new Error(`${name} react runtime export missing`);', - '}', - 'for (const name of expectedReactTypeOnlyExports) {', - ' if (name in zcr) throw new Error(`${name} type-only react export leaked at runtime`);', - '}', - 'if (typeof useJSONDocument !== "function") throw new Error("useJSONDocument export failed");', - ].join("\n"), - ); - await writeFile( - join(workspace, "react-smoke.ts"), - [ - 'import * as z from "zod";', - 'import type { JSONDocument } from "@interactive-os/json-document/session";', - 'import { useJSONDocument } from "@interactive-os/json-document/react";', - 'const Schema = z.object({ name: z.string() });', - 'type Value = z.output;', - 'type Doc = JSONDocument;', - 'const doc = useJSONDocument(Schema, { name: "ok" }, { history: 1 });', - 'doc satisfies Doc;', - ].join("\n"), - ); - await writeFile( - join(workspace, "readme-react-example.tsx"), - markdownCodeBlockAfterHeading(readmeSource, "React — `useJSONDocument`", "tsx"), - ); - const readmeTypeScriptExamplePaths = []; - const readmePureCoreExamples = markdownCodeBlocksAfterHeading(readmeSource, "순수 core", "ts"); - const readmeSerializationExamples = markdownCodeBlocksAfterHeading(readmeSource, "직렬화", "ts"); - if (readmePureCoreExamples.length !== 1 || readmeSerializationExamples.length !== 2) { - throw new Error( - `README package smoke must cover 1 pure core and 2 serialization TypeScript examples: pure=${readmePureCoreExamples.length} serialization=${readmeSerializationExamples.length}`, - ); - } - const readmeTypeScriptExamples = [...readmePureCoreExamples, ...readmeSerializationExamples]; - for (const [index, block] of readmeTypeScriptExamples.entries()) { - const filename = `readme-typescript-example-${index + 1}.ts`; - readmeTypeScriptExamplePaths.push(filename); - await writeFile(join(workspace, filename), block); - } - const [readmePureCoreExample] = readmePureCoreExamples; - const [readmeSerializationExample] = readmeSerializationExamples; - if (readmePureCoreExample === undefined || readmeSerializationExample === undefined) { - throw new Error("README runtime examples missing from package smoke"); + for (const required of ["README.md", "LICENSE", "public-contract.json", "package.json"]) { + if (!packedFiles.includes(required)) throw new Error(`Packed file missing: ${required}`); } - await writeFile( - join(workspace, "readme-pure-core-example.mjs"), - [ - readmePureCoreExample, - 'if (!r.ok) throw new Error("README pure core example did not apply");', - 'if (r.value.title !== "final" || r.value.tags[0] !== "docs") throw new Error("README pure core example state mismatch");', - ].join("\n"), - ); - await writeFile( - join(workspace, "readme-serialization-example.mjs"), - [ - readmeSerializationExample, - 'if (json !== JSON.stringify(state)) throw new Error("README serialization example JSON mismatch");', - 'if (restored.title !== "draft") throw new Error("README serialization example parse mismatch");', - 'if (!safe.success || safe.data.title !== "draft") throw new Error("README serialization example safeParse mismatch");', - ].join("\n"), - ); await writeFile( join(workspace, "package.json"), @@ -804,145 +174,116 @@ try { }, }, null, 2), ); - - run("npm", ["install", "--legacy-peer-deps", "--ignore-scripts", "--no-audit", "--no-fund", "--no-package-lock"], workspace); - await mkdir(join(workspace, "node_modules"), { recursive: true }); - const installedPackageRoot = join(workspace, "node_modules", "@interactive-os/json-document"); - assertInstalledPackageJson( - JSON.parse(await readFile(join(installedPackageRoot, "package.json"), "utf8")), - ); - await assertInstalledTextFiles(installedPackageRoot); - assertDeclarationExports( - await readFile(join(installedPackageRoot, packageJson.exports["."].types.replace(/^\.\//, "")), "utf8"), - rootPublicExports, - "root", + run( + "npm", + ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--no-package-lock"], + workspace, ); - assertDeclarationExports( - await readFile(join(installedPackageRoot, packageJson.exports["./session"].types.replace(/^\.\//, "")), "utf8"), - sessionPublicExports, - "session", + + const installedRoot = join( + workspace, + "node_modules", + "@interactive-os", + "json-document", ); - assertDeclarationExports( - await readFile(join(installedPackageRoot, packageJson.exports["./react"].types.replace(/^\.\//, "")), "utf8"), - reactPublicExports, - "react", + const installedPackage = JSON.parse( + await readFile(join(installedRoot, "package.json"), "utf8"), ); - await assertDeclarationSpecifiers(installedPackageRoot); - await assertRuntimeSpecifiers(installedPackageRoot); - - if (existsSync(join(workspace, "node_modules", "zod"))) { - throw new Error("Optional Zod peer must not be installed for the root smoke."); - } - run("node", ["root-smoke.mjs"], workspace); - - if (!existsSync(join(workspace, "node_modules", "zod"))) { - await symlink(zodPackage, join(workspace, "node_modules", "zod"), "dir"); - } - if (existsSync(join(workspace, "node_modules", "react"))) { - throw new Error("Root package smoke must run before React is installed so the root entrypoint stays headless"); + if (Object.keys(installedPackage.exports).join(",") !== ".") { + throw new Error("Installed package exposed a non-root entrypoint."); + } + if ( + installedPackage.dependencies !== undefined + || installedPackage.peerDependencies !== undefined + || installedPackage.peerDependenciesMeta !== undefined + ) { + throw new Error("Installed package contains runtime or peer dependencies."); + } + + const distRoot = join(installedRoot, "dist"); + const distFiles = await filesUnder(distRoot); + for (const file of distFiles) { + if ( + file.startsWith("application/session/") + || file.startsWith("application/react-document/") + || /^domain\/(?:clipboard|document|editing|schema|selection|text-surface)\//.test(file) + || file === "foundation/patch/schema.js" + || file === "foundation/patch/schema.d.ts" + || file === "foundation/patch/schema-contract.d.ts" + ) { + throw new Error(`Archived implementation leaked into dist: ${file}`); + } + const source = await readFile(join(distRoot, file), "utf8"); + for (const specifier of moduleSpecifiers(source)) { + if (!specifier.startsWith(".")) { + throw new Error(`External dependency leaked into dist: ${file} -> ${specifier}`); + } + } } - const privateSubpaths = [ - "package.json", - ...Object.values(packageJson.exports).map((exportMap) => exportMap.import.replace(/^\.\//, "")), - ]; - for (const privateSubpath of privateSubpaths) { - expectCommandFailure( - "node", - ["--input-type=module", "--eval", `await import("@interactive-os/json-document/${privateSubpath}")`], - workspace, - "ERR_PACKAGE_PATH_NOT_EXPORTED", + const rootDeclaration = await readFile( + join(installedRoot, packageJson.exports["."].types), + "utf8", + ); + const declaredExports = declarationExportNames(rootDeclaration); + if (JSON.stringify(declaredExports) !== JSON.stringify([...rootExports].sort())) { + throw new Error( + `Root declarations drifted:\nexpected ${[...rootExports].sort().join(", ")}\nactual ${declaredExports.join(", ")}`, ); } - run("node", ["session-smoke.mjs"], workspace); - run("node", ["subpath-smoke.mjs"], workspace); - run("node", ["readme-pure-core-example.mjs"], workspace); - run("node", ["readme-serialization-example.mjs"], workspace); - run( - "node", - [ - typeScriptBin, - "--noEmit", - "--target", - "ES2022", - "--module", - "NodeNext", - "--moduleResolution", - "NodeNext", - "--strict", - "--exactOptionalPropertyTypes", - "--noUncheckedIndexedAccess", - ...readmeTypeScriptExamplePaths, - "smoke.ts", - ], - workspace, - ); - run( - "node", - [ - typeScriptBin, - "--noEmit", - "--target", - "ES2022", - "--module", - "NodeNext", - "--moduleResolution", - "NodeNext", - "--strict", - "--exactOptionalPropertyTypes", - "--noUncheckedIndexedAccess", - "named-imports-smoke.ts", - ], - workspace, - ); - run( - "node", + await writeFile( + join(workspace, "runtime-smoke.mjs"), [ - typeScriptBin, - "--noEmit", - "--target", - "ES2022", - "--module", - "ESNext", - "--moduleResolution", - "Bundler", - "--strict", - "--exactOptionalPropertyTypes", - "--noUncheckedIndexedAccess", - ...readmeTypeScriptExamplePaths, - "smoke.ts", - "named-imports-smoke.ts", - ], - workspace, + 'import * as api from "@interactive-os/json-document";', + `const expected = ${JSON.stringify([...rootValueExports].sort())};`, + 'const actual = Object.keys(api).sort();', + 'if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`runtime exports: ${actual}`);', + 'const document = api.createJSONDocument({ title: "draft", tags: [] });', + 'if (JSON.stringify(Object.keys(document).sort()) !== JSON.stringify(["at", "canPatch", "commit", "query", "subscribe", "value"])) throw new Error("document surface drifted");', + 'const changes = [];', + 'document.subscribe((change) => changes.push(change));', + 'const result = document.commit([{ op: "add", path: "/tags/-", value: "v2" }]);', + 'if (!result.ok || document.value.tags[0] !== "v2" || changes.length !== 1) throw new Error("commit failed");', + 'const patched = api.applyPatch(document.value, [{ op: "replace", path: "/title", value: "stable" }]);', + 'if (!patched.ok || patched.value.title !== "stable") throw new Error("pure patch failed");', + ].join("\n"), ); + run("node", ["runtime-smoke.mjs"], workspace); - if (!existsSync(join(workspace, "node_modules", "react"))) { - await symlink(reactPackage, join(workspace, "node_modules", "react"), "dir"); - } - await mkdir(join(workspace, "node_modules", "@types"), { recursive: true }); - if (!existsSync(join(workspace, "node_modules", "@types", "react"))) { - await symlink(reactTypesPackage, join(workspace, "node_modules", "@types", "react"), "dir"); - } + for (const subpath of removedSubpaths) expectImportFailure(subpath); + const rootImportTarget = packageJson.exports["."].import.replace(/^\.\//, ""); + expectImportFailure(`@interactive-os/json-document/${rootImportTarget}`); + expectImportFailure("@interactive-os/json-document/package.json"); - run("node", ["react-smoke.mjs"], workspace); - run( - "node", + await writeFile( + join(workspace, "type-smoke.ts"), [ - typeScriptBin, - "--noEmit", - "--target", - "ES2022", - "--module", - "NodeNext", - "--moduleResolution", - "NodeNext", - "--strict", - "--exactOptionalPropertyTypes", - "--noUncheckedIndexedAccess", - "react-smoke.ts", - ], - workspace, + `import { ${rootValueExports.join(", ")} } from "@interactive-os/json-document";`, + `import type { ${rootTypeExports.join(", ")} } from "@interactive-os/json-document";`, + 'const document: JSONDocument = createJSONDocument({ text: "a" });', + 'type Members = keyof JSONDocument;', + 'const members: Members[] = ["value", "at", "query", "canPatch", "commit", "subscribe"];', + 'const operation: JSONPatchOperation = { op: "replace", path: "/text", value: "b" };', + 'const metadata: JSONChangeMetadata = { source: "type-smoke" };', + 'const result: JSONDocumentCommitResult = document.commit([operation], { metadata });', + 'const pure: JSONPatchResult = applyPatch(document.value, [operation]);', + 'const pointer: Pointer = appendSegment(buildPointer(["text"]), 0);', + 'const values: JSONValue[] = [document.value, pointer, null];', + 'void [members, result, pure, values, parsePointer, tryParsePointer, parentPointer, trackPointer];', + 'type Remaining = JSONAppliedChange | JSONCapabilityResult | JSONDocumentCommitOptions | QueryResult | ReadResult;', + 'declare const remaining: Remaining;', + 'void remaining;', + ].join("\n"), + ); + const typeScriptBin = resolve( + packageRoot, + "..", + "..", + "node_modules", + "typescript", + "bin", + "tsc", ); run( "node", @@ -955,58 +296,19 @@ try { "NodeNext", "--moduleResolution", "NodeNext", - "--jsx", - "react-jsx", "--strict", "--exactOptionalPropertyTypes", "--noUncheckedIndexedAccess", - "readme-react-example.tsx", + "type-smoke.ts", ], workspace, ); - run( - "node", - [ - typeScriptBin, - "--noEmit", - "--target", - "ES2022", - "--module", - "ESNext", - "--moduleResolution", - "Bundler", - "--strict", - "--exactOptionalPropertyTypes", - "--noUncheckedIndexedAccess", - "react-smoke.ts", - ], - workspace, - ); - run( - "node", - [ - typeScriptBin, - "--noEmit", - "--target", - "ES2022", - "--module", - "ESNext", - "--moduleResolution", - "Bundler", - "--jsx", - "react-jsx", - "--strict", - "--exactOptionalPropertyTypes", - "--noUncheckedIndexedAccess", - "readme-react-example.tsx", - ], - workspace, + + console.log( + `json-document package smoke ok: ${packResult.entryCount} files, ` + + `${(packResult.size / 1000).toFixed(1)} kB packed, ` + + `${(packResult.unpackedSize / 1000).toFixed(1)} kB unpacked`, ); } finally { - if (lockfileSnapshot === null) { - await rm(lockfilePath, { force: true }); - } else { - await writeFile(lockfilePath, lockfileSnapshot); - } await rm(workspace, { force: true, recursive: true }); } diff --git a/packages/json-document/tsconfig.json b/packages/json-document/tsconfig.json index e2cc84c6..1856d1a2 100644 --- a/packages/json-document/tsconfig.json +++ b/packages/json-document/tsconfig.json @@ -13,6 +13,6 @@ "outDir": "dist" }, "include": [ - "src/**/*.ts" + "src/application/document/index.ts" ] } diff --git a/scripts/benchmark-core.mjs b/scripts/benchmark-core.mjs index 975b1022..14e442c2 100644 --- a/scripts/benchmark-core.mjs +++ b/scripts/benchmark-core.mjs @@ -1,1626 +1,147 @@ import { existsSync } from "node:fs"; import { performance } from "node:perf_hooks"; -import * as z from "zod"; -const distKernelEntry = new URL("../packages/json-document/dist/application/document/index.js", import.meta.url); -const distSessionEntry = new URL("../packages/json-document/dist/application/session/index.js", import.meta.url); -const distJsonCloneEntry = new URL("../packages/json-document/dist/foundation/json/clone.js", import.meta.url); -const distJsonSerializableEntry = new URL("../packages/json-document/dist/foundation/json/serializable.js", import.meta.url); -const distJsonPathEntry = new URL("../packages/json-document/dist/foundation/jsonpath/index.js", import.meta.url); -const distJsonPathParseEntry = new URL("../packages/json-document/dist/foundation/jsonpath/parse.js", import.meta.url); -const distJsonPathEvaluateEntry = new URL("../packages/json-document/dist/foundation/jsonpath/evaluate.js", import.meta.url); -const distPatchEntry = new URL("../packages/json-document/dist/foundation/patch/trusted.js", import.meta.url); -const distPatchInverseEntry = new URL("../packages/json-document/dist/foundation/patch/inverse.js", import.meta.url); -const distHistoryEntry = new URL("../packages/json-document/dist/foundation/history/index.js", import.meta.url); +const publicEntry = new URL( + "../packages/json-document/dist/application/document/index.js", + import.meta.url, +); -if ( - !existsSync(distKernelEntry) - || !existsSync(distSessionEntry) - || !existsSync(distJsonCloneEntry) - || !existsSync(distJsonSerializableEntry) - || !existsSync(distJsonPathEntry) - || !existsSync(distJsonPathParseEntry) - || !existsSync(distJsonPathEvaluateEntry) - || !existsSync(distPatchEntry) - || !existsSync(distPatchInverseEntry) - || !existsSync(distHistoryEntry) -) { - console.error("Missing package dist. Run `npm run build -w @interactive-os/json-document` first."); +if (!existsSync(publicEntry)) { + console.error( + "Missing package dist. Run `npm run build -w @interactive-os/json-document` first.", + ); process.exit(1); } const { - applyPatch: applyKernelPatch, - createJSONDocument: createKernelDocument, -} = await import(distKernelEntry.href); -const { applyPatch, applyPatchToTrustedState, createJSONDocument } = await import(distSessionEntry.href); -const { cloneJsonSerializable } = await import(distJsonCloneEntry.href); -const { jsonSerializableError } = await import(distJsonSerializableEntry.href); -const { query: jsonpathQuery, queryMatches: jsonpathQueryMatches } = await import(distJsonPathEntry.href); -const { parse: parseJsonPath } = await import(distJsonPathParseEntry.href); -const { evaluate: evaluateJsonPath, matchPointers: matchJsonPathPointers } = await import(distJsonPathEvaluateEntry.href); -const { applyAcceptedPatch, applyTrustedPatch } = await import(distPatchEntry.href); -const { computeInverses } = await import(distPatchInverseEntry.href); -const { - commitMutable, - emptyMutableHistory, - moveBack, - moveForward, -} = await import(distHistoryEntry.href); - -const Item = z.object({ - id: z.string(), - title: z.string(), - done: z.boolean(), - value: z.number(), - meta: z.object({ - tag: z.string(), - rank: z.number(), - }), -}); -const Schema = z.object({ - items: z.array(Item), - settings: z.object({ - active: z.string(), - count: z.number(), - }), -}); -const OptionalItemsSchema = z.object({ - items: z.array(Item).optional(), - settings: z.object({ - active: z.string(), - count: z.number(), - }), -}); -const UnknownItemsSchema = z.object({ - items: z.array(z.unknown()), -}); -const NestedSchema = z.object({ - wrapper: z.object({ - items: z.array(Item), - }), -}); -const EscapedSelectionSchema = z.object({ - "a/b": z.array(z.object({ - "done~flag": z.boolean(), - })), -}); -const EscapedNestedSchema = z.object({ - "a/b": z.array(z.object({ - "m~eta": z.object({ - "ra/nk": z.number(), - }), - })), -}); -const RecursiveNode = z.lazy(() => z.object({ - id: z.string(), - children: z.array(RecursiveNode), -})); - -const sizes = envList("PERF_ITEMS", [10000, 50000]); -const batchSize = envNumber("PERF_BATCH", 1000); -const individualCount = envNumber("PERF_INDIVIDUAL", 100); -const jsonpathRepeats = envNumber("PERF_JSONPATH_REPEATS", 10000); -const rounds = envNumber("PERF_ROUNDS", 5); -const kernelCommitP50BudgetPerTenThousandMs = envNumber( - "PERF_KERNEL_COMMIT_P50_BUDGET_PER_10000_MS", + applyPatch, + createJSONDocument, +} = await import(publicEntry.href); + +const sizes = envList("PERF_ITEMS", [10_000, 50_000]); +const rounds = envPositiveInteger("PERF_ROUNDS", 5); +const warmups = envPositiveInteger("PERF_WARMUPS", 2); +const commitBudgetPerTenThousandMs = envPositiveNumber( + "PERF_COMMIT_P50_BUDGET_PER_10000_MS", 6, ); -const leafCommitP50BudgetMs = envNumber("PERF_LEAF_COMMIT_P50_BUDGET_MS", 0.75); -const leafSnapshotP50BudgetMs = envNumber("PERF_LEAF_SNAPSHOT_P50_BUDGET_MS", 4); -const frozenTrustedSnapshotP50BudgetMs = envNumber("PERF_FROZEN_TRUSTED_SNAPSHOT_P50_BUDGET_MS", 4); -const copyBatchP50BudgetPerThousandMs = envNumber("PERF_COPY_BATCH_P50_BUDGET_PER_1000_MS", 15); -const frozenTrustedCopySnapshotP50BudgetPerThousandMs = envNumber("PERF_FROZEN_TRUSTED_COPY_SNAPSHOT_P50_BUDGET_PER_1000_MS", 15); -const warmRootReplaceP50BudgetPerThousandMs = envNumber("PERF_WARM_ROOT_REPLACE_P50_BUDGET_PER_1000_MS", 20); -const overlappingReplaceRounds = Math.max(15, rounds); -const forceGc = process.env.PERF_GC === "1"; -const runtimeGc = typeof globalThis.gc === "function" ? globalThis.gc.bind(globalThis) : null; -console.log("json-document core benchmark"); -console.log(`items=${sizes.join(",")} batch=${batchSize} individual=${individualCount} rounds=${rounds}`); -if (forceGc) { - console.log(`gc=${runtimeGc ? "enabled" : "unavailable (run node with --expose-gc)"}`); -} +console.log("json-document v2 public-root benchmark"); +console.log(`items=${sizes.join(",")} rounds=${rounds} warmups=${warmups}`); for (const size of sizes) { - const state = Schema.parse(makeState(size)); - const frozenTrustedState = deepFreezeJson(Schema.parse(makeState(size))); - const optionalItemsState = OptionalItemsSchema.parse(state); - const nestedState = NestedSchema.parse({ wrapper: { items: state.items } }); - const escapedSelectionState = EscapedSelectionSchema.parse({ - "a/b": Array.from({ length: size }, () => ({ "done~flag": false })), - }); - const escapedNestedState = EscapedNestedSchema.parse({ - "a/b": Array.from({ length: size }, (_, index) => ({ "m~eta": { "ra/nk": index } })), - }); - const recursiveState = RecursiveNode.parse(makeRecursiveState(size)); - const primitiveArrayState = Array.from({ length: size }, (_, index) => index); + const initial = { + title: "benchmark", + items: Array.from({ length: size }, (_, index) => ({ + id: `item-${index}`, + done: false, + })), + }; const middle = Math.floor(size / 2); - const jsonpathOne = `$.items[${middle}].done`; - const jsonpathRegexFilter = '$.items[?search(@.title, "999")]'; - const batchOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ - op: "replace", - path: `/items/${index}/done`, - value: true, - })); - const escapedSelectionBatchOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ + const operation = { op: "replace", - path: `/a~1b/${index}/done~0flag`, + path: `/items/${middle}/done`, value: true, - })); - const repeatedFieldReplaceOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ - op: "replace", - path: "/items/0/done", - value: index % 2 === 0, - })); - const nestedFieldReplaceOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ - op: "replace", - path: `/items/${index}/meta/rank`, - value: size + index, - })); - const overlappingNestedReplaceOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => - index % 2 === 0 - ? { - op: "replace", - path: "/items/0/meta", - value: { tag: "overlap", rank: index }, - } - : { - op: "replace", - path: "/items/0/meta/rank", - value: index, - }); - const guardedOverlappingNestedReplaceOps = [ - { op: "test", path: "/items/0/meta", value: { tag: "tag-0", rank: 0 } }, - { op: "test", path: "/items/0/meta/rank", value: 0 }, - ...overlappingNestedReplaceOps, - ]; - const escapedNestedFieldReplaceOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ - op: "replace", - path: `/a~1b/${index}/m~0eta/ra~1nk`, - value: size + index, - })); - const addBatchOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ - op: "add", - path: "/items/-", - value: makeItem(size + index), - })); - const itemReplaceBatchOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ - op: "replace", - path: `/items/${index}`, - value: makeItem(size + index), - })); - const repeatedItemReplaceOps = Array.from({ length: Math.min(batchSize, size) }, (_, index) => ({ - op: "replace", - path: "/items/0", - value: makeItem(size + index), - })); - const insertedItems = Array.from({ length: Math.min(individualCount, size) }, (_, index) => makeItem(size + index)); - const repeatedRekeyItems = Array.from({ length: Math.min(individualCount, size) }, (_, index) => ({ - ...makeItem(index), - id: "id-0", - })); - const mixedArrayOps = [ - ...insertedItems.slice(0, Math.floor(insertedItems.length / 2)).map((item) => ({ - op: "add", - path: "/items/-", - value: item, - })), - ...Array.from({ length: insertedItems.length - Math.floor(insertedItems.length / 2) }, (_, index) => ({ - op: "remove", - path: `/items/${size - index - 1}`, - })), - ]; - const copyMoveCount = Math.min(individualCount, size); - const copyBatchOps = Array.from({ length: Math.min(batchSize, size) }, () => ({ - op: "copy", - from: "/items/0", - path: "/items/-", - })); - const copyMoveOps = [ - ...Array.from({ length: Math.floor(copyMoveCount / 2) }, (_, index) => ({ - op: "copy", - from: `/items/${index}`, - path: "/items/-", - })), - ...Array.from({ length: copyMoveCount - Math.floor(copyMoveCount / 2) }, () => ({ - op: "move", - from: "/items/1", - path: "/items/0", - })), - ]; - - console.log(`\nitems=${size}`); - bench("kernel createJSONDocument", Math.max(3, Math.ceil(rounds / 2)), () => { - const doc = createKernelDocument(state); - return { ok: doc.value.items.length === size }; - }); - bench("kernel applyPatch single leaf replace", rounds, (index) => - applyKernelPatch(state, [{ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - }])); - { - const doc = createKernelDocument(state); - const stats = bench("kernel commit single leaf replace", rounds, (index) => - doc.commit([{ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - }])); - assertP50Budget( - "kernel commit single leaf replace", - stats, - kernelCommitP50BudgetPerTenThousandMs * (size / 10000), - ); - } - bench("jsonSerializableError state", rounds, () => ({ ok: jsonSerializableError(state) === null })); - bench("cloneJsonSerializable state", Math.max(3, Math.ceil(rounds / 2)), () => cloneJsonSerializable(state)); - bench("cloneJsonSerializable primitive array", Math.max(3, Math.ceil(rounds / 2)), () => - cloneJsonSerializable(primitiveArrayState)); - bench("createJSONDocument init history=0", Math.max(3, Math.ceil(rounds / 2)), () => { - const doc = createJSONDocument(Schema, state, { history: 0 }); - return { ok: doc.value.items.length === size }; - }); - bench("createJSONDocument init mutable trustedInitial history=0", Math.max(3, Math.ceil(rounds / 2)), () => { - const doc = createJSONDocument(Schema, state, { history: 0, trustedInitial: true }); - return { ok: doc.value.items.length === size }; - }); - bench("createJSONDocument init frozen trustedInitial history=0", Math.max(3, Math.ceil(rounds / 2)), () => { - const doc = createJSONDocument(Schema, frozenTrustedState, { history: 0, trustedInitial: true }); - return { ok: doc.value === frozenTrustedState }; - }); - bench("createJSONDocument init history=100", Math.max(3, Math.ceil(rounds / 2)), () => { - const doc = createJSONDocument(Schema, state, { history: 100 }); - return { ok: doc.value.items.length === size }; - }); - bench("createJSONDocument lazy tree init history=0", Math.max(3, Math.ceil(rounds / 2)), () => { - const doc = createJSONDocument(RecursiveNode, recursiveState, { history: 0 }); - return { ok: doc.value.children.length === size }; - }); - bench("applyPatch single leaf replace", rounds, (index) => - applyPatch(Schema, state, [{ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - }]).result); - bench(`applyPatch repeated item replace batch ${repeatedItemReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyPatch(Schema, state, repeatedItemReplaceOps).result); - bench("applyPatchToTrustedState single leaf replace", rounds, (index) => - applyPatchToTrustedState(Schema, state, [{ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - }]).result); - bench(`applyPatchToTrustedState repeated item replace batch ${repeatedItemReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyPatchToTrustedState(Schema, state, repeatedItemReplaceOps).result); - bench(`applyPatchToTrustedState nested field replace batch ${nestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyPatchToTrustedState(Schema, state, nestedFieldReplaceOps).result); - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - const stats = bench("doc.patch single leaf + history", rounds, (index) => - doc.patch({ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - })); - assertP50Budget("leaf commit", stats, leafCommitP50BudgetMs); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - void doc.value; - const stats = bench("doc.patch single leaf + history + snapshot", rounds, (index) => { - const result = doc.patch({ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - }); - void doc.value; - return result; - }); - assertP50Budget("leaf commit + snapshot", stats, leafSnapshotP50BudgetMs); - } - - { - let doc; - const stats = benchWithSetup( - "doc.patch frozen trustedInitial leaf + first snapshot", - rounds, - () => { - doc = createJSONDocument(Schema, frozenTrustedState, { - history: 100, - trustedInitial: true, - }); - }, - () => { - const result = doc.patch({ - op: "replace", - path: `/items/${middle}/done`, - value: true, - }); - void doc.value; - return result; - }, - ); - assertP50Budget("frozen trustedInitial leaf + first snapshot", stats, frozenTrustedSnapshotP50BudgetMs); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench("doc.canPatch single leaf", rounds, (index) => - doc.canPatch({ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canDuplicate middle", rounds, () => - doc.canDuplicate(`/items/${middle}`)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canDuplicate middle + rekey", rounds, () => - doc.canDuplicate(`/items/${middle}`, { rekey: { fields: ["id"], strategy: "suffix" } })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.duplicate middle", rounds, () => - doc.duplicate(`/items/${middle}`)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.duplicate middle + rekey", rounds, () => - doc.duplicate(`/items/${middle}`, { rekey: { fields: ["id"], strategy: "suffix" } })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canMove adjacent", rounds, () => - doc.canMove(`/items/${middle}`, `/items/${middle + 1}`)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canReplace jsonpath one", rounds, (index) => - doc.canReplace(`$.items[${middle}].done`, index % 2 === 0)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canFind jsonpath wildcard", rounds, () => - doc.canFind("$.items[*].id")); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.query jsonpath wildcard", Math.max(3, Math.ceil(rounds / 2)), () => { - const result = doc.query("$.items[*].id"); - return { ok: result.ok && result.pointers.length === size }; - }); - } - - bench("jsonpath queryMatches wildcard field", Math.max(3, Math.ceil(rounds / 2)), () => ({ - ok: jsonpathQueryMatches("$.items[*].id", state).length === size, - })); - - bench(`jsonpath direct parse+evaluate ${jsonpathRepeats}`, Math.max(3, Math.ceil(rounds / 2)), () => { - let ok = true; - for (let index = 0; index < jsonpathRepeats; index += 1) { - const pointers = matchJsonPathPointers(evaluateJsonPath(parseJsonPath(jsonpathOne), state)); - ok = ok && pointers[0] === `/items/${middle}/done`; - } - return { ok }; - }); - - bench(`jsonpath cached query ${jsonpathRepeats}`, Math.max(3, Math.ceil(rounds / 2)), () => { - let ok = true; - for (let index = 0; index < jsonpathRepeats; index += 1) { - const pointers = jsonpathQuery(jsonpathOne, state); - ok = ok && pointers[0] === `/items/${middle}/done`; - } - return { ok }; - }); - bench("jsonpath regex filter search", Math.max(3, Math.ceil(rounds / 2)), () => ({ - ok: jsonpathQueryMatches(jsonpathRegexFilter, state).length > 0, - })); - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canCopy /items", Math.max(3, Math.ceil(rounds / 2)), () => - doc.canCopy("/items")); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canCut last item", Math.max(3, Math.ceil(rounds / 2)), () => - doc.canCut(`/items/${size - 1}`)); - } - - { - let doc; - benchWithSetup("doc.clipboard.cut /items", Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(OptionalItemsSchema, optionalItemsState, { history: 0 }); - }, () => doc.clipboard.cut("/items")); - } - - { - let doc; - benchWithSetup("doc.clipboard.cut /items no clone", Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(OptionalItemsSchema, optionalItemsState, { history: 0 }); - }, () => doc.clipboard.cut("/items", { clonePayload: false })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.canInsert direct payload single append", rounds, (index) => - doc.canInsert("/items/-", makeItem(size + index))); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.insert direct payload single append", rounds, (index) => - doc.insert("/items/-", makeItem(size + index))); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench(`doc.insert direct payload spread ${insertedItems.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.insert("/items/-", insertedItems, { spread: true })); - } - - { - let doc; - benchWithSetup(`doc.insert direct payload spread ${insertedItems.length} + rekey`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 0 }); - }, () => - doc.insert("/items/-", insertedItems, { - spread: true, - rekey: { fields: ["id"], strategy: "suffix" }, - })); - } - - { - let doc; - benchWithSetup(`doc.insert direct payload spread repeated rekey ${repeatedRekeyItems.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 0 }); - }, () => - doc.insert("/items/-", repeatedRekeyItems, { - spread: true, - rekey: { fields: ["id"], strategy: "suffix" }, - })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench(`doc.insert direct payload spread ${insertedItems.length} before middle`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.insert(`/items/${middle}`, insertedItems, { spread: true })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.clipboard.copy /items", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.copy("/items")); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.clipboard.copy /items no clone", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.copy("/items", { clonePayload: false })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.clipboard.write items payload", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.write(state.items)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.clipboard.write items payload trusted", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.write(state.items, { trustedPayload: true })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.clipboard.write items payload trusted no clone", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.write(state.items, { trustedPayload: true, clonePayload: false })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.clipboard.write items payload validate no clone", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.write(state.items, { clonePayload: false })); - } - - { - const doc = createJSONDocument(UnknownItemsSchema, { items: [] }, { history: 0 }); - bench("doc.replace direct payload unknown /items", Math.max(3, Math.ceil(rounds / 2)), () => - doc.replace("/items", state.items)); - } - - { - const doc = createJSONDocument(UnknownItemsSchema, { items: [] }, { history: 0 }); - bench("doc.clipboard.write trusted + paste replace /items", Math.max(3, Math.ceil(rounds / 2)), () => { - doc.clipboard.write(state.items, { trustedPayload: true, clonePayload: false }); - return doc.clipboard.paste({ replace: "/items" }); - }); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench("doc.clipboard.write document items payload", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.write(doc.value.items)); - } - - { - const doc = createJSONDocument(NestedSchema, nestedState, { history: 0 }); - bench("doc.clipboard.write nested source items payload", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.write(doc.value.wrapper.items, { source: "/wrapper/items" })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - const written = doc.clipboard.write(state.items); - if (!written.ok) throw new Error(`clipboard write setup failed: ${JSON.stringify(written)}`); - bench("doc.clipboard.read items payload", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.read()); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - const written = doc.clipboard.write(state.items, { trustedPayload: true, clonePayload: false }); - if (!written.ok) throw new Error(`clipboard write setup failed: ${JSON.stringify(written)}`); - bench("doc.clipboard.read items payload no clone", Math.max(3, Math.ceil(rounds / 2)), () => - doc.clipboard.read({ clonePayload: false })); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - const copied = doc.clipboard.copy("/items"); - if (!copied.ok) throw new Error(`clipboard copy setup failed: ${JSON.stringify(copied)}`); - bench("doc.canPaste clipboard replace /items", Math.max(3, Math.ceil(rounds / 2)), () => - doc.canPaste({ replace: "/items" })); - } - - { - const doc = createJSONDocument(Schema, state, { - history: 100, - selection: { mode: "single", initial: [`/items/${middle}/done`] }, - }); - const selection = doc.selection?.snapshot(); - bench("doc.commit single leaf + selectionAfter", rounds, (index) => - doc.commit([{ - op: "replace", - path: `/items/${middle}/done`, - value: index % 2 === 0, - }], selection ? { selectionAfter: selection } : undefined)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench(`doc.patch batch ${batchOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(batchOps)); - } - - { - const selectedIndex = Math.min(batchOps.length - 1, size - 1); - const doc = createJSONDocument(Schema, state, { - history: 0, - selection: { mode: "single", initial: [`/items/${selectedIndex}/done`] }, - }); - bench(`doc.patch batch ${batchOps.length} selection=single`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(batchOps)); - } - - { - const selectedIndex = Math.min(escapedSelectionBatchOps.length - 1, size - 1); - const doc = createJSONDocument(EscapedSelectionSchema, escapedSelectionState, { - history: 0, - selection: { mode: "single", initial: [`/a~1b/${selectedIndex}/done~0flag`] }, - }); - bench(`doc.patch escaped batch ${escapedSelectionBatchOps.length} selection=single`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(escapedSelectionBatchOps)); - } - - { - const selectedCount = Math.min(500, batchOps.length, size); - const selected = Array.from({ length: selectedCount }, (_, index) => `/items/${index}/done`); - const doc = createJSONDocument(Schema, state, { - history: 0, - selection: { mode: "multiple" }, - }); - doc.selection?.selectRanges(selected); - bench(`doc.patch batch ${batchOps.length} selection=multiple-${selectedCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(batchOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench(`doc.patch batch ${batchOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(batchOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench(`doc.patch repeated field replace batch ${repeatedFieldReplaceOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(repeatedFieldReplaceOps)); - } - - bench(`accepted nested field replace batch ${nestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyAcceptedPatch(state, nestedFieldReplaceOps)); - bench(`trusted nested field replace batch ${nestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyTrustedPatch(state, nestedFieldReplaceOps, { valuesTrusted: true })); - bench(`computeInverses nested field replace batch ${nestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - computeInverses(state, nestedFieldReplaceOps)); - bench(`computeInverses overlapping nested replace batch ${overlappingNestedReplaceOps.length}`, overlappingReplaceRounds, () => - computeInverses(state, overlappingNestedReplaceOps)); - bench(`accepted escaped nested field replace batch ${escapedNestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyAcceptedPatch(escapedNestedState, escapedNestedFieldReplaceOps)); - bench(`trusted escaped nested field replace batch ${escapedNestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyTrustedPatch(escapedNestedState, escapedNestedFieldReplaceOps, { valuesTrusted: true })); - bench(`computeInverses escaped nested field replace batch ${escapedNestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => - computeInverses(escapedNestedState, escapedNestedFieldReplaceOps)); - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench(`doc.patch nested field replace batch ${nestedFieldReplaceOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(nestedFieldReplaceOps)); - } - - { - let doc; - benchWithSetup(`doc.patch overlapping nested replace batch ${overlappingNestedReplaceOps.length} history=0`, overlappingReplaceRounds, () => { - doc = createJSONDocument(Schema, state, { history: 0, trustedInitial: true }); - }, () => doc.patch(overlappingNestedReplaceOps)); - } - { - let doc; - benchWithSetup(`doc.patch overlapping nested replace batch ${overlappingNestedReplaceOps.length} history=100`, overlappingReplaceRounds, () => { - doc = createJSONDocument(Schema, state, { history: 100, trustedInitial: true }); - }, () => doc.patch(overlappingNestedReplaceOps)); - } - { - let doc; - benchWithSetup(`doc.patch guarded overlapping nested replace batch ${guardedOverlappingNestedReplaceOps.length} history=0`, overlappingReplaceRounds, () => { - doc = createJSONDocument(Schema, state, { history: 0, trustedInitial: true }); - }, () => doc.patch(guardedOverlappingNestedReplaceOps)); - } - { - let doc; - benchWithSetup(`doc.patch guarded overlapping nested replace batch ${guardedOverlappingNestedReplaceOps.length} history=100`, overlappingReplaceRounds, () => { - doc = createJSONDocument(Schema, state, { history: 100, trustedInitial: true }); - }, () => doc.patch(guardedOverlappingNestedReplaceOps)); - } - { - const doc = createJSONDocument(Schema, state, { history: 100, trustedInitial: true }); - const result = doc.patch(overlappingNestedReplaceOps); - if (!result.ok) throw new Error(`setup overlapping nested replace batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo overlapping nested replace batch ${overlappingNestedReplaceOps.length}`, overlappingReplaceRounds, () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("overlapping nested replace redo setup failed"); - } - }, () => ({ ok: doc.history.undo() })); - benchWithSetup(`history redo overlapping nested replace batch ${overlappingNestedReplaceOps.length}`, overlappingReplaceRounds, () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("overlapping nested replace undo setup failed"); - } - }, () => ({ ok: doc.history.redo() })); - } - { - const doc = createJSONDocument(EscapedNestedSchema, escapedNestedState, { history: 0 }); - bench(`doc.patch escaped nested field replace batch ${escapedNestedFieldReplaceOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(escapedNestedFieldReplaceOps)); - } - - { - const selectedIndex = Math.min(nestedFieldReplaceOps.length - 1, size - 1); - const doc = createJSONDocument(Schema, state, { - history: 0, - selection: { mode: "single", initial: [`/items/${selectedIndex}/meta/rank`] }, - }); - bench(`doc.patch nested field replace batch ${nestedFieldReplaceOps.length} selection=single`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(nestedFieldReplaceOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench(`doc.patch nested field replace batch ${nestedFieldReplaceOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(nestedFieldReplaceOps)); - } - { - const doc = createJSONDocument(EscapedNestedSchema, escapedNestedState, { history: 100 }); - bench(`doc.patch escaped nested field replace batch ${escapedNestedFieldReplaceOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(escapedNestedFieldReplaceOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - const result = doc.patch(nestedFieldReplaceOps); - if (!result.ok) throw new Error(`setup nested field replace batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo nested field replace batch ${nestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("nested field replace redo setup failed"); - } - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo nested field replace batch ${nestedFieldReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("nested field replace undo setup failed"); - } - }, () => { - return { ok: doc.history.redo() }; - }); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench(`doc.patch item replace batch ${itemReplaceBatchOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(itemReplaceBatchOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench(`doc.patch item replace batch ${itemReplaceBatchOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(itemReplaceBatchOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench(`doc.patch repeated item replace batch ${repeatedItemReplaceOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(repeatedItemReplaceOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - const result = doc.patch(repeatedItemReplaceOps); - if (!result.ok) throw new Error(`setup repeated item replace batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo repeated item replace batch ${repeatedItemReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("repeated item replace redo setup failed"); - } - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo repeated item replace batch ${repeatedItemReplaceOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("repeated item replace undo setup failed"); - } - }, () => { - return { ok: doc.history.redo() }; - }); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - const result = doc.patch(itemReplaceBatchOps); - if (!result.ok) throw new Error(`setup item replace batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo item replace batch ${itemReplaceBatchOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("item replace redo setup failed"); - } - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo item replace batch ${itemReplaceBatchOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("item replace undo setup failed"); - } - }, () => { - return { ok: doc.history.redo() }; - }); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - const result = doc.patch(batchOps); - if (!result.ok) throw new Error(`setup batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo batch ${batchOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("redo setup failed"); - } - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo batch ${batchOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("undo setup failed"); - } - }, () => { - return { ok: doc.history.redo() }; - }); - } - - { - let doc; - benchWithSetup(`doc.patch add batch ${addBatchOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 0 }); - }, () => doc.patch(addBatchOps)); - } - - { - let doc; - benchWithSetup(`doc.patch add batch ${addBatchOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 100 }); - }, () => doc.patch(addBatchOps)); - } - - { - let doc; - benchWithSetup(`history undo add batch ${addBatchOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 100 }); - const patched = doc.patch(addBatchOps); - if (!patched.ok) throw new Error(`setup add batch failed: ${JSON.stringify(patched)}`); - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo add batch ${addBatchOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 100 }); - const patched = doc.patch(addBatchOps); - if (!patched.ok) throw new Error(`setup add batch failed: ${JSON.stringify(patched)}`); - const undone = doc.history.undo(); - if (!undone) throw new Error("add batch undo setup failed"); - }, () => { - return { ok: doc.history.redo() }; - }); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`doc.patch individual ${count} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 0 }); - }, () => { - patchDoneRange(doc, count, "individual patch"); - return { ok: true }; - }); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`doc.patch individual ${count} history=${individualCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: individualCount }); - }, () => { - patchDoneRange(doc, count, "individual patch"); - return { ok: true }; - }); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`doc.history.transaction individual ${count} history=${individualCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: individualCount }); - }, () => { - patchDoneTransaction(doc, count, "transaction patch"); - return { ok: doc.history.undoDepth === 1 }; - }); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`history undo individual ${count} entries`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: count }); - patchDoneRange(doc, count, "setup individual patch"); - }, () => { - undoAll(doc, count, "individual undo"); - return { ok: doc.history.undoDepth === 0 }; - }); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`history redo individual ${count} entries`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: count }); - patchDoneRange(doc, count, "setup individual patch"); - undoAll(doc, count, "setup individual undo"); - }, () => { - redoAll(doc, count, "individual redo"); - return { ok: doc.history.redoDepth === 0 }; - }); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`history undo transaction individual ${count}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: count }); - patchDoneTransaction(doc, count, "setup transaction patch"); - }, () => ({ ok: doc.history.undo() })); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`history redo transaction individual ${count}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: count }); - patchDoneTransaction(doc, count, "setup transaction patch"); - if (!doc.history.undo()) throw new Error("setup transaction undo failed"); - }, () => ({ ok: doc.history.redo() })); - } - - { - let doc; - benchWithSetup(`doc.patch individual add ${insertedItems.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 0 }); - }, () => { - for (let index = 0; index < insertedItems.length; index++) { - const result = doc.patch({ - op: "add", - path: "/items/-", - value: insertedItems[index], - }); - if (!result.ok) throw new Error(`individual add failed: ${JSON.stringify(result)}`); - } - return { ok: true }; - }); - } - - { - let doc; - const count = Math.min(individualCount, size); - benchWithSetup(`doc.patch individual remove ${count} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 0 }); - }, () => { - for (let index = 0; index < count; index++) { - const result = doc.patch({ - op: "remove", - path: `/items/${size - index - 1}`, - }); - if (!result.ok) throw new Error(`individual remove failed: ${JSON.stringify(result)}`); - } - return { ok: true }; - }); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench(`doc.patch mixed array batch ${mixedArrayOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(mixedArrayOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench(`doc.patch mixed array batch ${mixedArrayOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(mixedArrayOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - const result = doc.patch(mixedArrayOps); - if (!result.ok) throw new Error(`setup mixed batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo mixed array batch ${mixedArrayOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("mixed redo setup failed"); - } - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo mixed array batch ${mixedArrayOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("mixed undo setup failed"); - } - }, () => { - return { ok: doc.history.redo() }; - }); - } - - { - let doc; - benchWithSetup(`doc.patch individual copy/move ${copyMoveOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(Schema, state, { history: 0 }); - }, () => { - for (const op of copyMoveOps) { - const result = doc.patch(op); - if (!result.ok) throw new Error(`individual copy/move failed: ${JSON.stringify(result)}`); - } - return { ok: true }; - }); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - const stats = bench(`doc.patch copy array batch ${copyBatchOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(copyBatchOps)); - assertP50Budget( - `copy batch ${copyBatchOps.length}`, - stats, - copyBatchP50BudgetPerThousandMs * Math.max(1, copyBatchOps.length / 1000), - ); - } - - { - let doc; - const stats = benchWithSetup( - `doc.patch frozen trustedInitial copy batch ${copyBatchOps.length} + first snapshot`, - Math.max(3, Math.ceil(rounds / 2)), - () => { - doc = createJSONDocument(Schema, frozenTrustedState, { - history: 0, - trustedInitial: true, - }); - }, - () => { - const result = doc.patch(copyBatchOps); - void doc.value; - return result; - }, - ); - assertP50Budget( - `frozen trustedInitial copy batch ${copyBatchOps.length} + first snapshot`, - stats, - frozenTrustedCopySnapshotP50BudgetPerThousandMs * Math.max(1, copyBatchOps.length / 1000), - ); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0 }); - bench(`doc.patch copy/move array batch ${copyMoveOps.length} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(copyMoveOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0, selection: true }); - bench(`doc.patch copy/move array batch ${copyMoveOps.length} selection=single`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(copyMoveOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0, selection: { mode: "multiple" } }); - bench(`doc.patch copy/move array batch ${copyMoveOps.length} selection=multiple`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(copyMoveOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 0, selection: { mode: "multiple" } }); - bench(`doc.patch add batch ${addBatchOps.length} selection=multiple`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(addBatchOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - bench(`doc.patch copy/move array batch ${copyMoveOps.length} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(copyMoveOps)); - } - - { - const doc = createJSONDocument(Schema, state, { history: 100 }); - const result = doc.patch(copyMoveOps); - if (!result.ok) throw new Error(`setup copy/move batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo copy/move array batch ${copyMoveOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("copy/move redo setup failed"); - } - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo copy/move array batch ${copyMoveOps.length}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("copy/move undo setup failed"); - } - }, () => { - return { ok: doc.history.redo() }; - }); - } -} - -{ - const rootReplaceCount = envNumber("PERF_ROOT_KEYS", batchSize); - const RootValue = z.object({ - id: z.string(), - done: z.boolean(), - meta: z.object({ - rank: z.number(), - tag: z.string(), - }), - }); - const RootRecord = z.record(z.string(), RootValue); - const rootState = makeRootObjectState(rootReplaceCount); - const rootReplaceOps = Array.from({ length: rootReplaceCount }, (_, index) => ({ - op: "replace", - path: `/k${index}`, - value: makeRootObjectValue(rootReplaceCount + index), - })); - const rootAddOps = Array.from({ length: rootReplaceCount }, (_, index) => ({ - op: "add", - path: `/n${index}`, - value: makeRootObjectValue(rootReplaceCount + index), - })); - const rootSmallAddOp = { - op: "add", - path: "/small", - value: makeRootObjectValue(rootReplaceCount * 3), }; - const rootRemoveOps = Array.from({ length: rootReplaceCount }, (_, index) => ({ - op: "remove", - path: `/k${index}`, - })); - const rootNestedReplaceCount = Math.min(batchSize, rootReplaceCount); - const rootNestedReplaceOps = Array.from({ length: rootNestedReplaceCount }, (_, index) => ({ - op: "replace", - path: `/k${index}/meta/rank`, - value: rootReplaceCount + index, - })); - const wideRootKeyCount = envNumber("PERF_WIDE_ROOT_KEYS", Math.max(...sizes)); - const wideRootState = makeRootObjectState(wideRootKeyCount); - const wideRootReplaceCount = Math.min(batchSize, wideRootKeyCount); - const wideRootReplaceOps = Array.from({ length: wideRootReplaceCount }, (_, index) => ({ - op: "replace", - path: `/k${index}`, - value: makeRootObjectValue(wideRootKeyCount + index), - })); - console.log(`\nroot keys=${rootReplaceCount}`); - bench(`createJSONDocument root record init history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - const doc = createJSONDocument(RootRecord, rootState, { history: 0 }); - return { ok: Object.keys(doc.value).length === rootReplaceCount }; - }); - bench(`applyPatch root object replace batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyPatch(RootRecord, rootState, rootReplaceOps).result); - { - let doc; - benchWithSetup(`doc.patch root nested replace batch ${rootNestedReplaceCount} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 0, trustedInitial: true }); - }, () => doc.patch(rootNestedReplaceOps)); - } - bench(`accepted root nested replace batch ${rootNestedReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyAcceptedPatch(rootState, rootNestedReplaceOps)); - bench(`trusted root nested replace batch ${rootNestedReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyTrustedPatch(rootState, rootNestedReplaceOps, { valuesTrusted: true })); - { - const doc = createJSONDocument(RootRecord, rootState, { history: 0 }); - bench(`doc.patch root object replace batch ${rootReplaceCount} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(rootReplaceOps)); - } - { - let doc; - const stats = benchWithSetup( - `doc.patch warm wide root ${wideRootKeyCount} replace batch ${wideRootReplaceCount}`, - Math.max(3, Math.ceil(rounds / 2)), - () => { - doc = createJSONDocument(RootRecord, wideRootState, { history: 0 }); - void doc.value; - }, - () => doc.patch(wideRootReplaceOps), - ); - assertP50Budget( - `warm wide root replace batch ${wideRootReplaceCount}`, - stats, - warmRootReplaceP50BudgetPerThousandMs * Math.max(1, wideRootReplaceCount / 1000), - ); - } - { - const doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - bench(`doc.patch root object replace batch ${rootReplaceCount} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => - doc.patch(rootReplaceOps)); - } - { - const doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - const result = doc.patch(rootReplaceOps); - if (!result.ok) throw new Error(`setup root replace batch failed: ${JSON.stringify(result)}`); - benchWithSetup(`history undo root object replace batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canUndo) { - const redone = doc.history.redo(); - if (!redone) throw new Error("root replace redo setup failed"); - } - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo root object replace batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - if (!doc.history.canRedo) { - const undone = doc.history.undo(); - if (!undone) throw new Error("root replace undo setup failed"); - } - }, () => { - return { ok: doc.history.redo() }; - }); - } - bench(`accepted root object replace batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyAcceptedPatch(rootState, rootReplaceOps)); - bench(`trusted root object replace batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyTrustedPatch(rootState, rootReplaceOps, { valuesTrusted: true })); - { - let doc; - benchWithSetup(`doc.patch root object add batch ${rootReplaceCount} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 0 }); - }, () => doc.patch(rootAddOps)); - } - { - let doc; - benchWithSetup(`doc.patch root object add batch ${rootReplaceCount} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - }, () => doc.patch(rootAddOps)); - } - { - let doc; - benchWithSetup(`history undo root object add batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - const patched = doc.patch(rootAddOps); - if (!patched.ok) throw new Error(`setup root add batch failed: ${JSON.stringify(patched)}`); - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo root object add batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - const patched = doc.patch(rootAddOps); - if (!patched.ok) throw new Error(`setup root add batch failed: ${JSON.stringify(patched)}`); - const undone = doc.history.undo(); - if (!undone) throw new Error("root add undo setup failed"); - }, () => { - return { ok: doc.history.redo() }; - }); - benchHeapRetained(`history root object add redo then small patch retained heap ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - const subject = createJSONDocument(RootRecord, rootState, { history: 100 }); - const patched = subject.patch(rootAddOps); - if (!patched.ok) throw new Error(`setup root add batch failed: ${JSON.stringify(patched)}`); - const undone = subject.history.undo(); - if (!undone) throw new Error("root add undo setup failed"); - const redone = subject.history.redo(); - if (!redone) throw new Error("root add redo setup failed"); - const result = subject.patch(rootSmallAddOp); - return { subject, result }; - }); - } - bench(`accepted root object add batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyAcceptedPatch(rootState, rootAddOps)); - bench(`trusted root object add batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyTrustedPatch(rootState, rootAddOps, { valuesTrusted: true })); - { - let doc; - benchWithSetup(`doc.patch root object remove batch ${rootReplaceCount} history=0`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 0 }); - }, () => doc.patch(rootRemoveOps)); - } - { - let doc; - benchWithSetup(`doc.patch root object remove batch ${rootReplaceCount} history=100`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - }, () => doc.patch(rootRemoveOps)); - } - { - let doc; - benchWithSetup(`history undo root object remove batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - const patched = doc.patch(rootRemoveOps); - if (!patched.ok) throw new Error(`setup root remove batch failed: ${JSON.stringify(patched)}`); - }, () => { - return { ok: doc.history.undo() }; - }); - benchWithSetup(`history redo root object remove batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => { - doc = createJSONDocument(RootRecord, rootState, { history: 100 }); - const patched = doc.patch(rootRemoveOps); - if (!patched.ok) throw new Error(`setup root remove batch failed: ${JSON.stringify(patched)}`); - const undone = doc.history.undo(); - if (!undone) throw new Error("root remove undo setup failed"); - }, () => { - return { ok: doc.history.redo() }; - }); - } - bench(`accepted root object remove batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyAcceptedPatch(rootState, rootRemoveOps)); - bench(`trusted root object remove batch ${rootReplaceCount}`, Math.max(3, Math.ceil(rounds / 2)), () => - applyTrustedPatch(rootState, rootRemoveOps, { valuesTrusted: true })); -} - -{ - const historyDepth = envNumber("PERF_HISTORY_DEPTH", 10000); - const historyEdits = envNumber("PERF_HISTORY_EDITS", historyDepth * 5); - const transactionEdits = envNumber("PERF_TRANSACTION_EDITS", historyDepth * 2); - const TinySchema = z.object({ value: z.number() }); - const doc = createJSONDocument(TinySchema, { value: 0 }, { history: historyDepth }); - const commitElapsed = time(() => { - for (let index = 1; index <= historyDepth; index += 1) { - const result = doc.patch({ op: "replace", path: "/value", value: index }); - if (!result.ok) throw new Error(`history depth patch failed: ${JSON.stringify(result)}`); - } - }); - const undoElapsed = time(() => { - for (let index = 0; index < historyDepth; index += 1) { - if (!doc.history.undo()) throw new Error("history depth undo failed"); - } - }); - const redoElapsed = time(() => { - for (let index = 0; index < historyDepth; index += 1) { - if (!doc.history.redo()) throw new Error("history depth redo failed"); - } - }); - console.log(`history depth ${historyDepth} commit: ${commitElapsed.toFixed(2)}ms`); - console.log(`history depth ${historyDepth} undo all: ${undoElapsed.toFixed(2)}ms`); - console.log(`history depth ${historyDepth} redo all: ${redoElapsed.toFixed(2)}ms`); - const overflowDoc = createJSONDocument(TinySchema, { value: 0 }, { history: historyDepth }); - const overflowElapsed = time(() => { - for (let index = 1; index <= historyEdits; index += 1) { - const result = overflowDoc.patch({ op: "replace", path: "/value", value: index }); - if (!result.ok) throw new Error(`history overflow patch failed: ${JSON.stringify(result)}`); - } - }); - console.log(`history limit ${historyDepth} overflow ${historyEdits} commits: ${overflowElapsed.toFixed(2)}ms`); + console.log(`\nitems=${size}`); - const transactionDoc = createJSONDocument(TinySchema, { value: 0 }, { history: transactionEdits + 1 }); - const transactionElapsed = time(() => { - transactionDoc.history.transaction(() => { - for (let index = 1; index <= transactionEdits; index += 1) { - const result = transactionDoc.patch({ op: "replace", path: "/value", value: index }); - if (!result.ok) throw new Error(`transaction patch failed: ${JSON.stringify(result)}`); - } - }); - }); - if (transactionDoc.history.undoDepth !== 1) { - throw new Error(`transaction history merge failed: ${transactionDoc.history.undoDepth}`); - } - const transactionUndoElapsed = time(() => { - if (!transactionDoc.history.undo()) throw new Error("transaction undo failed"); + measure("createJSONDocument", () => { + const document = createJSONDocument(initial); + return document.value.items.length === size; }); - const transactionRedoElapsed = time(() => { - if (!transactionDoc.history.redo()) throw new Error("transaction redo failed"); - }); - console.log(`history transaction ${transactionEdits} commits: ${transactionElapsed.toFixed(2)}ms`); - console.log(`history transaction ${transactionEdits} undo: ${transactionUndoElapsed.toFixed(2)}ms`); - console.log(`history transaction ${transactionEdits} redo: ${transactionRedoElapsed.toFixed(2)}ms`); - const mergeLastDoc = createJSONDocument(TinySchema, { value: 0 }, { history: transactionEdits + 1 }); - const mergeLastElapsed = time(() => { - for (let index = 1; index <= transactionEdits; index += 1) { - const result = mergeLastDoc.patch({ op: "replace", path: "/value", value: index }); - if (!result.ok) throw new Error(`mergeLast patch failed: ${JSON.stringify(result)}`); - if (index > 1 && !mergeLastDoc.history.mergeLast()) throw new Error("mergeLast failed"); - } + measure("applyPatch single leaf replace", () => { + const result = applyPatch(initial, [operation]); + return result.ok && result.value.items[middle].done === true; }); - if (mergeLastDoc.history.undoDepth !== 1) { - throw new Error(`mergeLast history merge failed: ${mergeLastDoc.history.undoDepth}`); - } - console.log(`history mergeLast repeated replace ${transactionEdits} commits: ${mergeLastElapsed.toFixed(2)}ms`); - const reducerStack = emptyMutableHistory(); - const reducerCommitElapsed = time(() => { - for (let index = 1; index <= historyEdits; index += 1) { - commitMutable(reducerStack, { index }, historyDepth); - } - }); - const reducerUndoElapsed = time(() => { - for (let index = 0; index < historyDepth; index += 1) { - moveBack(reducerStack); - } - }); - const reducerRedoElapsed = time(() => { - for (let index = 0; index < historyDepth; index += 1) { - moveForward(reducerStack); - } + const commitDocument = createJSONDocument(initial); + let nextDone = true; + const commit = measure("commit single leaf replace", () => { + const result = commitDocument.commit([{ + ...operation, + value: nextDone, + }]); + nextDone = !nextDone; + return result.ok; }); - console.log(`history reducer limit ${historyDepth} overflow ${historyEdits} commits: ${reducerCommitElapsed.toFixed(2)}ms`); - console.log(`history reducer depth ${historyDepth} undo all: ${reducerUndoElapsed.toFixed(2)}ms`); - console.log(`history reducer depth ${historyDepth} redo all: ${reducerRedoElapsed.toFixed(2)}ms`); -} - -function makeState(size) { - return { - items: Array.from({ length: size }, (_, index) => makeItem(index)), - settings: { - active: "main", - count: size, - }, - }; -} - -function makeItem(index) { - return { - id: `id-${index}`, - title: `item ${index}`, - done: false, - value: index, - meta: { - tag: `tag-${index % 10}`, - rank: index % 100, - }, - }; -} - -function makeRecursiveState(size) { - return { - id: "root", - children: Array.from({ length: size }, (_, index) => ({ - id: `id-${index}`, - children: [], - })), - }; -} - -function deepFreezeJson(value) { - if (value === null || typeof value !== "object") return value; - const stack = [value]; - while (stack.length > 0) { - const current = stack.pop(); - if (Object.isFrozen(current)) continue; - for (const key of Object.keys(current)) { - const child = current[key]; - if (child !== null && typeof child === "object") stack.push(child); - } - Object.freeze(current); - } - return value; -} - -function makeRootObjectState(size) { - return Object.fromEntries( - Array.from({ length: size }, (_, index) => [`k${index}`, makeRootObjectValue(index)]), + assertBudget( + "commit single leaf replace", + commit.p50, + commitBudgetPerTenThousandMs * (size / 10_000), ); -} - -function makeRootObjectValue(index) { - return { - id: `id-${index}`, - done: false, - meta: { - rank: index, - tag: `tag-${index % 10}`, - }, - }; -} -function patchDoneRange(doc, count, label) { - for (let index = 0; index < count; index++) { - const result = doc.patch({ - op: "replace", - path: `/items/${index}/done`, - value: true, - }); - if (!result.ok) throw new Error(`${label} failed: ${JSON.stringify(result)}`); - } -} - -function patchDoneTransaction(doc, count, label) { - doc.history.transaction(() => { - patchDoneRange(doc, count, label); + const queryDocument = createJSONDocument(initial); + measure("query direct item", () => { + const result = queryDocument.query(`$.items[${middle}].id`); + return result.ok && result.pointers[0] === `/items/${middle}/id`; }); } -function undoAll(doc, count, label) { - for (let index = 0; index < count; index++) { - if (!doc.history.undo()) throw new Error(`${label} failed at ${index}`); - } -} - -function redoAll(doc, count, label) { - for (let index = 0; index < count; index++) { - if (!doc.history.redo()) throw new Error(`${label} failed at ${index}`); - } -} - -function bench(label, sampleCount, fn) { +function measure(label, run) { const samples = []; - let last; - for (let index = 0; index < sampleCount; index++) { - maybeCollectGarbage(); - const started = performance.now(); - last = fn(index); - samples.push(performance.now() - started); - } - const { avg, min, p50, p90, max } = sampleStats(samples); - const ok = resultOk(last); - console.log(`${label}: avg=${avg.toFixed(2)}ms min=${min.toFixed(2)}ms p50=${p50.toFixed(2)}ms p90=${p90.toFixed(2)}ms max=${max.toFixed(2)}ms ok=${ok}`); - return { avg, min, p50, p90, max, ok }; -} - -function assertP50Budget(label, stats, limitMs) { - const pass = stats.p50 <= limitMs; - console.log(`budget ${label}: p50=${stats.p50.toFixed(2)}ms limit=${limitMs.toFixed(2)}ms pass=${pass}`); - if (!pass) process.exitCode = 1; + for (let index = 0; index < rounds + warmups; index += 1) { + const start = performance.now(); + const ok = run(); + const duration = performance.now() - start; + if (!ok) throw new Error(`${label} returned an invalid result.`); + if (index >= warmups) samples.push(duration); + } + samples.sort((left, right) => left - right); + const result = { + p50: percentile(samples, 0.5), + p95: percentile(samples, 0.95), + }; + console.log( + `${label}: p50=${result.p50.toFixed(3)}ms p95=${result.p95.toFixed(3)}ms`, + ); + return result; } -function benchWithSetup(label, sampleCount, setup, fn) { - const samples = []; - let last; - for (let index = 0; index < sampleCount; index++) { - setup(index); - maybeCollectGarbage(); - const started = performance.now(); - last = fn(index); - samples.push(performance.now() - started); - } - const { avg, min, p50, p90, max } = sampleStats(samples); - const ok = resultOk(last); - console.log(`${label}: avg=${avg.toFixed(2)}ms min=${min.toFixed(2)}ms p50=${p50.toFixed(2)}ms p90=${p90.toFixed(2)}ms max=${max.toFixed(2)}ms ok=${ok}`); - return { avg, min, p50, p90, max, ok }; +function percentile(sorted, quantile) { + if (sorted.length === 0) throw new Error("No benchmark samples."); + const index = Math.min( + sorted.length - 1, + Math.max(0, Math.ceil(sorted.length * quantile) - 1), + ); + return sorted[index]; } -function benchHeapRetained(label, sampleCount, fn) { - const samples = []; - const subjects = []; - const cleanups = []; - let last; - maybeCollectGarbage(); - const baseline = process.memoryUsage().heapUsed; - for (let index = 0; index < sampleCount; index++) { - const retained = fn(index); - last = retained.result; - subjects.push(retained.subject); - cleanups.push(retained.cleanup); - maybeCollectGarbage(); - samples.push((process.memoryUsage().heapUsed - baseline) / subjects.length); - } - for (let index = 0; index < subjects.length; index += 1) { - const cleanup = cleanups[index]; - if (typeof cleanup === "function") cleanup(subjects[index]); +function assertBudget(label, actual, budget) { + if (actual > budget) { + throw new Error( + `${label} p50 ${actual.toFixed(3)}ms exceeds ${budget.toFixed(3)}ms budget.`, + ); } - subjects.length = 0; - const { avg, min, p50, p90, max } = sampleStats(samples); - const ok = resultOk(last); - console.log(`${label}: avg=${formatBytes(avg)} min=${formatBytes(min)} p50=${formatBytes(p50)} p90=${formatBytes(p90)} max=${formatBytes(max)} ok=${ok}`); + console.log(`${label} budget: ${actual.toFixed(3)}ms <= ${budget.toFixed(3)}ms`); } -function resultOk(value) { - if (typeof value !== "object" || value === null) return "n/a"; - if ("ok" in value) return value.ok; +function envList(name, fallback) { + const value = process.env[name]; + if (value === undefined || value.trim() === "") return fallback; + const parsed = value.split(",").map((entry) => Number(entry.trim())); if ( - "result" in value - && typeof value.result === "object" - && value.result !== null - && "ok" in value.result + parsed.length === 0 + || parsed.some((entry) => !Number.isSafeInteger(entry) || entry <= 0) ) { - return value.result.ok; + throw new RangeError(`${name} must be a comma-separated list of positive integers.`); } - return "n/a"; -} - -function formatBytes(value) { - return `${(value / 1024 / 1024).toFixed(2)}MB`; -} - -function sampleStats(samples) { - const sorted = [...samples].sort((left, right) => left - right); - return { - avg: samples.reduce((total, sample) => total + sample, 0) / samples.length, - min: sorted[0], - p50: sorted[Math.floor(sorted.length / 2)], - p90: sorted[Math.floor(sorted.length * 0.9)], - max: sorted[sorted.length - 1], - }; -} - -function time(fn) { - maybeCollectGarbage(); - const started = performance.now(); - fn(); - return performance.now() - started; -} - -function maybeCollectGarbage() { - if (forceGc) runtimeGc?.(); + return parsed; } -function envList(name, fallback) { - const value = process.env[name]; - if (!value) return fallback; - return value.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item) && item > 0); +function envPositiveInteger(name, fallback) { + const parsed = Number(process.env[name] ?? fallback); + if (!Number.isSafeInteger(parsed) || parsed <= 0) { + throw new RangeError(`${name} must be a positive integer.`); + } + return parsed; } -function envNumber(name, fallback) { - const value = Number(process.env[name]); - return Number.isFinite(value) && value > 0 ? value : fallback; +function envPositiveNumber(name, fallback) { + const parsed = Number(process.env[name] ?? fallback); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new RangeError(`${name} must be a positive number.`); + } + return parsed; } diff --git a/scripts/evaluate-docs.mjs b/scripts/evaluate-docs.mjs index a2754e7c..8a9347da 100644 --- a/scripts/evaluate-docs.mjs +++ b/scripts/evaluate-docs.mjs @@ -81,6 +81,18 @@ const publicContract = JSON.parse(read("packages/json-document/public-contract.j const officialExtensions = officialExtensionNames(); const generatedOfficialExtensions = generatedDocs.repoCatalog.officialExtensions.map((item) => item.name).sort(); +for (const [name, source] of Object.entries({ + rootReadme: surfaces.rootReadme, + readme: surfaces.readme, + llms: surfaces.llms, + ...publicDocs, + siteHome, +})) { + if (/@interactive-os\/json-document\/(?:session|react)\b/.test(source)) { + fail(`${name}: removed v2 package entrypoint is still documented.`); + } +} + for (const removedPath of [ "docs/release/evaluation-loop.md", "docs/release/notes.md", @@ -221,20 +233,13 @@ const required = [ ["overview", /## 이걸로 할 수 있는 것들/], ["quickstart", /튜토리얼: 작은 카드 편집기 만들기/], ["quickstart", /JSONPath는 변경 언어가 아닙니다/], - ["quickstart", /Pointer 배열을 copy하면 clipboard payload도 배열/], + ["quickstart", /Selection, clipboard, history, DOM lifecycle/], ["api", /## 작업별 진입점/], ["api", /ReadResult/], - ["api", /canFind/], - ["api", /violations\[\]\.path/], - ["api", /schema-slot/], - ["api", /document-result/], ["api", /Root document Pointer는 빈 문자열 `""`/], ["api", /function asPointer/], ["api", /applyPatch[\s\S]*외부 JSON 경계/], - ["api", /신뢰된 document state/], - ["api", /구조만 가진 Zod schema/], - ["api", /전체 루트 schema 검증/], - ["api", /기본값은 `strict: false`/], + ["api", /## Host와 adapter/], ["extensions", /@interactive-os\/json-document-collection/], ["extensions", /@interactive-os\/json-document-clipboard-web/], ["extensions", /@interactive-os\/json-document-outline/], @@ -265,11 +270,11 @@ const required = [ ["rootReadme", /labs\/extensions/], ["rootReadme", /provider-neutral/], ["rootReadme", /docs\/standard\/v2-projection-profile\.md/], - ["rootReadme", /Candidate Editing Session/], + ["rootReadme", /selection, clipboard, history, framework lifecycle/], ["docsReadme", /## 규범 우선순위/], ["docsReadme", /현재 v2 portable root의 정본/], ["coreStandard", /1\.x Candidate Editing Session migration baseline/], - ["v2Profile", /provider-neutral Projection construction boundary/], + ["v2Profile", /root entrypoint 하나와 20개 Kernel symbol/], ["v2Profile", /Acceptance callback[\s\S]*`canPatch`[\s\S]*`commit`/], ["clipboardReadme", /@interactive-os\/json-document\/session/], ["clipboardReadme", /six-member v2 Root\s+Projection adapter/], @@ -277,8 +282,7 @@ const required = [ ["readme", /npm install @interactive-os\/json-document@2\.0\.0-rc\.0/], ["readme", /provider-neutral/], ["readme", /## 공개 root/], - ["readme", /## Editing Session/], - ["readme", /React — `useJSONDocument`/], + ["readme", /패키지는 `\/session`이나 `\/react` subpath를\s*공개하지 않습니다/], ["readme", /순수 core/], ["readme", /직렬화/], ["llms", /2\.0\.0-rc\.0.*Candidate/], @@ -288,7 +292,7 @@ const required = [ ["llms", /`value`는 항상\s+`JSONValue`/], ["llms", /not_serializable/], ["llms", /Acceptance는 initial state와 commit candidate/], - ["llms", /Root `JSONDocument`의 subtype이라고 가정하지 않는다/], + ["llms", /패키지는 root entrypoint만 공개한다/], ["llms", /Tree[\s\S]*제품\s+의도는 host/], ["llms", /docs\/standard\/v2-projection-profile\.md/], ["llms", /docs\/standard\/v2-public-surface\.json/], @@ -343,8 +347,13 @@ for (const [name, pattern] of required) { if (!pattern.test(source)) fail(`${name}: missing ${pattern}.`); } -if (!publicContract.root.values.includes("createJSONDocument") || !publicContract.react.values.includes("useJSONDocument")) { - fail("public contract: missing required root or react value export."); +if ( + JSON.stringify(Object.keys(publicContract)) !== JSON.stringify(["root"]) + || publicContract.root.values.length !== 8 + || publicContract.root.types.length !== 12 + || !publicContract.root.values.includes("createJSONDocument") +) { + fail("public contract: v2 root must be the only 20-symbol entrypoint."); } if ( @@ -372,7 +381,7 @@ for (const pattern of [ /Provider-neutral JSON editing/, /six-member document projection/, /npm install @interactive-os\/json-document@2\.0\.0-rc\.0/, - /@interactive-os\/json-document\/session/, + /Rich editing belongs to host adapters/, ]) { if (!pattern.test(siteHome)) fail(`site home: missing ${pattern}.`); } diff --git a/scripts/evaluate-standardization.mjs b/scripts/evaluate-standardization.mjs index c74f1ae1..97c0a001 100644 --- a/scripts/evaluate-standardization.mjs +++ b/scripts/evaluate-standardization.mjs @@ -1,76 +1,146 @@ -import { readFileSync } from "node:fs"; -import { join } from "node:path"; - -const root = new URL("..", import.meta.url).pathname; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const failures = []; + +const EXPECTED_VALUES = [ + "appendSegment", + "applyPatch", + "buildPointer", + "createJSONDocument", + "parentPointer", + "parsePointer", + "trackPointer", + "tryParsePointer", +]; +const EXPECTED_TYPES = [ + "JSONAppliedChange", + "JSONCapabilityResult", + "JSONChangeMetadata", + "JSONDocument", + "JSONDocumentCommitOptions", + "JSONDocumentCommitResult", + "JSONPatchOperation", + "JSONPatchResult", + "JSONValue", + "Pointer", + "QueryResult", + "ReadResult", +]; +const EXPECTED_MEMBERS = [ + "value", + "at", + "query", + "canPatch", + "commit", + "subscribe", +]; +const EXPECTED_CONFORMANCE = { + projectionVectors: "packages/json-document/tests/conformance/v2/projection-vectors.json", + projectionSuite: "packages/json-document/tests/conformance/v2/projection-suite.ts", + projectionBinding: "packages/json-document/tests/public/v2-projection-standard-conformance.test.ts", + protocolVectors: "packages/json-document/tests/conformance/v2/protocol-vectors.json", + protocolSuite: "packages/json-document/tests/conformance/v2/protocol-suite.ts", + protocolBinding: "packages/json-document/tests/public/v2-protocol-standard-conformance.test.ts", + pointerVectors: "packages/json-document/tests/conformance/v2/pointer-vectors.json", + pointerSuite: "packages/json-document/tests/conformance/v2/pointer-suite.ts", + pointerBinding: "packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts", + rfc6902Suite: "packages/json-document/tests/conformance/v2/rfc6902-suite.ts", + rfc6902Binding: "packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts", + jsonPathSuite: "packages/json-document/tests/conformance/v2/jsonpath-suite.ts", + jsonPathBinding: "packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts", +}; function read(path) { - return readFileSync(join(root, path), "utf8"); + return readFileSync(join(repoRoot, path), "utf8"); +} + +function json(path) { + return JSON.parse(read(path)); } function fail(message) { - console.error(message); - process.exitCode = 1; + failures.push(message); } -function requirePattern(name, source, pattern) { - if (!pattern.test(source)) fail(`${name}: missing ${pattern}.`); +function equal(label, actual, expected) { + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + fail(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}.`); + } } -const V2_DISPOSITIONS = ["kernel", "session", "compat", "remove"]; -const V2_PROJECTION_MEMBERS = [ - "value", - "at", - "query", - "canPatch", - "commit", - "subscribe", -]; +function setEqual(label, actual, expected) { + const actualSet = new Set(actual); + const expectedSet = new Set(expected); + for (const value of expectedSet) { + if (!actualSet.has(value)) fail(`${label}: missing ${value}.`); + } + for (const value of actualSet) { + if (!expectedSet.has(value)) fail(`${label}: unexpected ${value}.`); + } +} -function publicSymbolKey(entrypoint, kind, name) { - return `${entrypoint}:${kind}:${name}`; +function requirePattern(label, source, pattern) { + if (!pattern.test(source)) fail(`${label}: missing ${pattern}.`); } -function compareSets(label, expected, actual) { - for (const value of expected) { - if (!actual.has(value)) fail(`${label}: missing ${value}.`); - } - for (const value of actual) { - if (!expected.has(value)) fail(`${label}: unexpected ${value}.`); +function publicExports(source) { + const values = []; + const types = []; + for (const match of source.matchAll(/\bexport\s+(type\s+)?\{([^}]*)\}/g)) { + const target = match[1] === undefined ? values : types; + target.push( + ...match[2] + .split(",") + .map((part) => part.trim()) + .filter(Boolean) + .map((part) => { + const normalized = part.replace(/^type\s+/, ""); + return normalized.match(/\s+as\s+([A-Za-z_$][\w$]*)$/)?.[1] + ?? normalized.split(/\s+/)[0]; + }), + ); } + return { + values: [...new Set(values)].sort(), + types: [...new Set(types)].sort(), + }; } -function extractExpectedDocumentMembers(source) { - const header = "interface ExpectedJSONDocument {"; - const headerIndex = source.indexOf(header); - if (headerIndex === -1) { - fail("v2 public surface: ExpectedJSONDocument was not found."); - return new Set(); +function interfaceMembers(source, interfaceName) { + const header = `export interface ${interfaceName} {`; + const start = source.indexOf(header); + if (start === -1) { + fail(`declaration: ${interfaceName} was not found.`); + return []; } - const bodyStart = headerIndex + header.length; + const bodyStart = start + header.length; let bodyEnd = -1; - let interfaceDepth = 1; + let depth = 1; for (let index = bodyStart; index < source.length; index += 1) { - if (source[index] === "{") interfaceDepth += 1; - if (source[index] === "}") interfaceDepth -= 1; - if (interfaceDepth === 0) { + if (source[index] === "{") depth += 1; + if (source[index] === "}") depth -= 1; + if (depth === 0) { bodyEnd = index; break; } } if (bodyEnd === -1) { - fail("v2 public surface: ExpectedJSONDocument is not closed."); - return new Set(); + fail(`declaration: ${interfaceName} is not closed.`); + return []; } - const members = new Set(); + const members = []; let parentheses = 0; let braces = 0; let brackets = 0; for (const line of source.slice(bodyStart, bodyEnd).split("\n")) { if (parentheses === 0 && braces === 0 && brackets === 0) { - const member = /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*(?=[:(])/.exec(line)?.[1]; - if (member !== undefined) members.add(member); + const name = /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*(?=[:(])/.exec(line)?.[1]; + if (name !== undefined) members.push(name); } for (const character of line) { if (character === "(") parentheses += 1; @@ -84,65 +154,207 @@ function extractExpectedDocumentMembers(source) { return members; } -function validateV2Conformance( - draft, - projectionVectors, - projectionSuiteSource, - projectionReferenceSource, - protocolVectors, - protocolSuiteSource, - protocolReferenceSource, - pointerVectors, - pointerSuiteSource, - pointerReferenceSource, - rfc6902SuiteSource, - rfc6902ReferenceSource, - jsonPathSuiteSource, - jsonPathReferenceSource, - requirementSet, -) { - const expectedArtifacts = { - projectionVectors: "packages/json-document/tests/conformance/v2/projection-vectors.json", - projectionSuite: "packages/json-document/tests/conformance/v2/projection-suite.ts", - projectionBinding: "packages/json-document/tests/public/v2-projection-standard-conformance.test.ts", - protocolVectors: "packages/json-document/tests/conformance/v2/protocol-vectors.json", - protocolSuite: "packages/json-document/tests/conformance/v2/protocol-suite.ts", - protocolBinding: "packages/json-document/tests/public/v2-protocol-standard-conformance.test.ts", - pointerVectors: "packages/json-document/tests/conformance/v2/pointer-vectors.json", - pointerSuite: "packages/json-document/tests/conformance/v2/pointer-suite.ts", - pointerBinding: "packages/json-document/tests/public/v2-pointer-standard-conformance.test.ts", - rfc6902Suite: "packages/json-document/tests/conformance/v2/rfc6902-suite.ts", - rfc6902Binding: "packages/json-document/tests/public/v2-rfc6902-standard-conformance.test.ts", - jsonPathSuite: "packages/json-document/tests/conformance/v2/jsonpath-suite.ts", - jsonPathBinding: "packages/json-document/tests/public/v2-jsonpath-standard-conformance.test.ts", - }; - if (JSON.stringify(draft.conformance) !== JSON.stringify(expectedArtifacts)) { - fail("v2 conformance: artifact paths changed without updating the evaluator."); +function assertGenericSuite(label, source, harnessPattern) { + if ( + /@interactive-os\/json-document|from\s+["']zod["']|\/src\//.test(source) + ) { + fail(`${label}: injected suite must be implementation and provider independent.`); + } + requirePattern(label, source, harnessPattern); +} + +function assertPublicBinding(label, source, pattern) { + if (!/from "@interactive-os\/json-document"/.test(source)) { + fail(`${label}: binding must import the public root package.`); } - if (projectionVectors.formatVersion !== 1 || projectionVectors.status !== "candidate") { - fail("v2 Projection vectors: expected formatVersion 1 and candidate status."); + if (/from ["'][^"']*(?:\/src\/|domain\/|foundation\/)|\/src\//.test(source)) { + fail(`${label}: private source path leaked.`); } - if (projectionVectors.profile !== "docs/standard/v2-projection-profile.md") { - fail("v2 Projection vectors: unexpected profile."); + requirePattern(label, source, pattern); +} + +const manifest = json("docs/standard/v2-public-surface.json"); +const profile = read("docs/standard/v2-projection-profile.md"); +const packageContract = json("packages/json-document/public-contract.json"); +const packageManifest = json("packages/json-document/package.json"); +const buildConfig = json("packages/json-document/tsconfig.json"); +const rootSource = read("packages/json-document/src/application/document/index.ts"); +const contractSource = read("packages/json-document/src/application/document/contract.ts"); +const signatureSource = read( + "packages/json-document/tests/public/v2-signature-contract.test-d.ts", +); +const packageSmoke = read("packages/json-document/tests/smoke/package-smoke.mjs"); +const coreBenchmark = read("scripts/benchmark-core.mjs"); + +if (manifest.formatVersion !== 1 || manifest.status !== "candidate") { + fail("v2 manifest: expected formatVersion 1 and candidate status."); +} +if (manifest.sourceContract !== "packages/json-document/public-contract.json#root") { + fail("v2 manifest: sourceContract must point to the root public contract."); +} +equal("v2 manifest projection members", manifest.projectionMembers, EXPECTED_MEMBERS); +equal("v2 manifest conformance paths", manifest.conformance, EXPECTED_CONFORMANCE); +equal("v2 manifest binding values", manifest.binding?.values, EXPECTED_VALUES); +equal("v2 manifest binding types", manifest.binding?.types, EXPECTED_TYPES); +equal("v2 manifest counts", manifest.counts, { + values: 8, + types: 12, + exports: 20, + projectionMembers: 6, +}); +equal("v2 package identity", { + name: manifest.package?.name, + version: manifest.package?.version, + entrypoint: manifest.package?.entrypoint, +}, { + name: packageManifest.name, + version: packageManifest.version, + entrypoint: ".", +}); +equal( + "v2 excluded entrypoints", + manifest.package?.excludedEntrypoints, + ["./session", "./react"], +); +equal("v2 runtime dependencies", manifest.package?.runtimeDependencies, []); +equal("v2 peer dependencies", manifest.package?.peerDependencies, []); + +equal("public contract entrypoints", Object.keys(packageContract), ["root"]); +equal("public contract values", packageContract.root?.values, EXPECTED_VALUES); +equal("public contract types", packageContract.root?.types, EXPECTED_TYPES); + +const sourceExports = publicExports(rootSource); +equal("root source values", sourceExports.values, [...EXPECTED_VALUES].sort()); +equal("root source types", sourceExports.types, [...EXPECTED_TYPES].sort()); +equal( + "JSONDocument members", + interfaceMembers(contractSource, "JSONDocument"), + EXPECTED_MEMBERS, +); + +equal("package entrypoints", Object.keys(packageManifest.exports ?? {}), ["."]); +equal("package export conditions", packageManifest.exports?.["."], { + types: "./dist/application/document/index.d.ts", + import: "./dist/application/document/index.js", +}); +if (packageManifest.main !== packageManifest.exports?.["."].import) { + fail("package: main must equal the root import target."); +} +if (packageManifest.types !== packageManifest.exports?.["."].types) { + fail("package: types must equal the root declaration target."); +} +for (const field of [ + "dependencies", + "optionalDependencies", + "peerDependencies", + "peerDependenciesMeta", +]) { + if (packageManifest[field] !== undefined) { + fail(`package: ${field} must be absent from the v2 kernel.`); } +} +equal( + "package files", + packageManifest.files, + ["dist", "README.md", "public-contract.json", "LICENSE"], +); +equal( + "production build roots", + buildConfig.include, + ["src/application/document/index.ts"], +); + +const documentedRequirements = [ + ...profile.matchAll(/\bJD2-[A-Z]+-\d{3}\b/g), +].map((match) => match[0]); +if (documentedRequirements.length !== new Set(documentedRequirements).size) { + fail("v2 profile: every requirement ID must appear exactly once."); +} +setEqual( + "v2 requirements", + documentedRequirements, + manifest.requirements ?? [], +); +for (const word of ["MUST", "SHOULD", "MAY"]) { + requirePattern("v2 profile normative language", profile, new RegExp(`\\b${word}\\b`)); +} +for (const pattern of [ + /Pure Protocol -> Projection -> host adapter/, + /root entrypoint 하나와 20개 Kernel symbol/, + /runtime dependency와 peer dependency가 없다/, + /archived 1\.x implementation은 production build와 tarball에\s*포함하지 않는다/, +]) { + requirePattern("v2 profile package closure", profile, pattern); +} + +requirePattern( + "v2 signature exact document", + signatureSource, + /"value" \| "at" \| "query" \| "canPatch" \| "commit" \| "subscribe"/, +); +requirePattern( + "v2 signature JSON boundary", + signatureSource, + /document\.value satisfies JSONValue/, +); +if ( + /@interactive-os\/json-document\/(?:session|react)|JSONDocument|document\.value\.id/.test( + signatureSource, + ) +) { + fail("v2 signature: archived entrypoint or unsound document generic leaked."); +} + +for (const path of Object.values(EXPECTED_CONFORMANCE)) { + if (!existsSync(join(repoRoot, path))) fail(`v2 conformance artifact missing: ${path}.`); +} + +const projectionVectors = json(manifest.conformance.projectionVectors); +const protocolVectors = json(manifest.conformance.protocolVectors); +const pointerVectors = json(manifest.conformance.pointerVectors); +const projectionSuite = read(manifest.conformance.projectionSuite); +const projectionBinding = read(manifest.conformance.projectionBinding); +const protocolSuite = read(manifest.conformance.protocolSuite); +const protocolBinding = read(manifest.conformance.protocolBinding); +const pointerSuite = read(manifest.conformance.pointerSuite); +const pointerBinding = read(manifest.conformance.pointerBinding); +const rfc6902Suite = read(manifest.conformance.rfc6902Suite); +const rfc6902Binding = read(manifest.conformance.rfc6902Binding); +const jsonPathSuite = read(manifest.conformance.jsonPathSuite); +const jsonPathBinding = read(manifest.conformance.jsonPathBinding); + +for (const [label, vectors] of [ + ["projection", projectionVectors], + ["protocol", protocolVectors], + ["pointer", pointerVectors], +]) { if ( - JSON.stringify(projectionVectors.projectionMembers) - !== JSON.stringify(V2_PROJECTION_MEMBERS) + vectors.formatVersion !== 1 + || vectors.status !== "candidate" + || vectors.profile !== "docs/standard/v2-projection-profile.md" ) { - fail("v2 Projection vectors: Projection members drifted."); + fail(`${label} vectors: metadata drifted.`); } - if ( - protocolVectors.formatVersion !== 1 - || protocolVectors.status !== "candidate" - || protocolVectors.profile !== "docs/standard/v2-projection-profile.md" - || protocolVectors.function !== "applyPatch" - ) { - fail("v2 Protocol vectors: metadata drifted."); +} +equal("projection vector members", projectionVectors.projectionMembers, EXPECTED_MEMBERS); +if (protocolVectors.function !== "applyPatch") { + fail("protocol vectors: function must be applyPatch."); +} +equal( + "pointer vector requirements", + pointerVectors.requirements, + ["JD2-ADDRESS-001", "JD2-PATCH-001"], +); +for (const group of ["parse", "invalid", "build", "append", "parent", "track"]) { + if (!Array.isArray(pointerVectors[group]) || pointerVectors[group].length === 0) { + fail(`pointer vectors: ${group} must be non-empty.`); } +} - const runtimeRequirements = new Set(); - const vectorIds = new Set(); - const allowedKinds = new Set([ +const requirementSet = new Set(manifest.requirements ?? []); +const runtimeRequirements = new Set(); +const allowedVectorKinds = { + projection: new Set([ "surface", "read", "commit", @@ -152,841 +364,189 @@ function validateV2Conformance( "non-json", "reentrant-publication", "subscriber-error", - ]); - if (!Array.isArray(projectionVectors.vectors)) { - fail("v2 conformance vectors: vectors must be an array."); - } - for (const vector of projectionVectors.vectors ?? []) { - if (typeof vector.id !== "string" || vector.id.trim() === "") { - fail("v2 conformance vectors: every vector needs an id."); - } else if (vectorIds.has(vector.id)) { - fail(`v2 conformance vectors: duplicate vector id ${vector.id}.`); - } else { - vectorIds.add(vector.id); - } - if (!allowedKinds.has(vector.kind)) { - fail(`v2 conformance vectors: ${vector.id} has unknown kind ${vector.kind}.`); - } - if (!Array.isArray(vector.requirements) || vector.requirements.length === 0) { - fail(`v2 conformance vectors: ${vector.id} needs requirement traceability.`); - continue; - } - for (const requirementId of vector.requirements) { - if (!requirementSet.has(requirementId)) { - fail(`v2 conformance vectors: ${vector.id} references unknown ${requirementId}.`); - } - runtimeRequirements.add(requirementId); - } - } - - const protocolKinds = new Set([ + ]), + protocol: new Set([ "patch", "extra-fields", "non-json-state", "non-json-payload", "precedence", "isolation", - ]); - const protocolVectorIds = new Set(); - for (const vector of protocolVectors.vectors ?? []) { + ]), +}; +for (const [suiteName, vectors] of [ + ["projection", projectionVectors.vectors ?? []], + ["protocol", protocolVectors.vectors ?? []], +]) { + const vectorIds = new Set(); + for (const vector of vectors) { if (typeof vector.id !== "string" || vector.id.trim() === "") { - fail("v2 Protocol vectors: every vector needs an id."); - } else if (protocolVectorIds.has(vector.id)) { - fail(`v2 Protocol vectors: duplicate vector id ${vector.id}.`); + fail(`${suiteName} vectors: every vector needs an id.`); + } else if (vectorIds.has(vector.id)) { + fail(`${suiteName} vectors: duplicate id ${vector.id}.`); } else { - protocolVectorIds.add(vector.id); + vectorIds.add(vector.id); } - if (!protocolKinds.has(vector.kind)) { - fail(`v2 Protocol vectors: ${vector.id} has unknown kind ${vector.kind}.`); + if (!allowedVectorKinds[suiteName].has(vector.kind)) { + fail(`${suiteName} vectors: ${vector.id} has unknown kind ${vector.kind}.`); } if (!Array.isArray(vector.requirements) || vector.requirements.length === 0) { - fail(`v2 Protocol vectors: ${vector.id} needs requirement traceability.`); - continue; + fail(`${suiteName} vectors: ${vector.id} needs requirement traceability.`); } - for (const requirementId of vector.requirements) { - if (!requirementSet.has(requirementId)) { - fail(`v2 Protocol vectors: ${vector.id} references unknown ${requirementId}.`); + for (const id of vector.requirements ?? []) { + if (!requirementSet.has(id)) { + fail(`${suiteName} vectors: ${vector.id} references unknown ${id}.`); } - runtimeRequirements.add(requirementId); + runtimeRequirements.add(id); } } +} - const nonRuntimeRequirements = new Map(); - const coverageByMode = { static: 0, deferred: 0 }; - for (const entry of projectionVectors.nonRuntimeRequirements ?? []) { - if (!requirementSet.has(entry.id)) { - fail(`v2 conformance vectors: unknown non-runtime requirement ${entry.id}.`); - } - if (runtimeRequirements.has(entry.id)) { - fail(`v2 conformance vectors: ${entry.id} is both runtime and non-runtime.`); - } - if (nonRuntimeRequirements.has(entry.id)) { - fail(`v2 conformance vectors: duplicate non-runtime requirement ${entry.id}.`); - } - if (!["static", "deferred"].includes(entry.mode)) { - fail(`v2 conformance vectors: ${entry.id} has invalid mode ${entry.mode}.`); - } else { - coverageByMode[entry.mode] += 1; - } - if (typeof entry.reason !== "string" || entry.reason.trim() === "") { - fail(`v2 conformance vectors: ${entry.id} needs a reason.`); - } - nonRuntimeRequirements.set(entry.id, entry.mode); - } - compareSets( - "v2 conformance requirement coverage", - requirementSet, - new Set([...runtimeRequirements, ...nonRuntimeRequirements.keys()]), - ); - compareSets( - "v2 conformance coverage modes", - new Set(["runtime", "static", "deferred"]), - new Set(Object.keys(projectionVectors.coverage ?? {})), - ); - if (projectionVectors.coverage?.runtime !== runtimeRequirements.size) { - fail("v2 conformance vectors: runtime coverage count drifted."); - } - if (projectionVectors.coverage?.static !== coverageByMode.static) { - fail("v2 conformance vectors: static coverage count drifted."); - } - if (projectionVectors.coverage?.deferred !== coverageByMode.deferred) { - fail("v2 conformance vectors: deferred coverage count drifted."); - } - if ( - /"_zod"\s*:|"safeParse"\s*:/.test( - JSON.stringify([projectionVectors, protocolVectors]), - ) - ) { - fail("v2 conformance vectors: provider object leaked into machine-readable data."); - } - - if ( - /@interactive-os\/json-document|from\s+["']zod["']|\/src\//.test( - projectionSuiteSource, - ) - ) { - fail("v2 conformance suite: generic runner must be implementation and provider independent."); - } - for (const [label, pattern] of [ - ["injected harness", /ProjectionHarness[\s\S]*runProjectionConformance/], - ["six-member contract", /readonly value[\s\S]*at\([\s\S]*query\([\s\S]*canPatch\([\s\S]*commit\([\s\S]*subscribe\(/], - ["required-field matching", /toMatchObject/], - ]) { - requirePattern(`v2 Projection suite ${label}`, projectionSuiteSource, pattern); - } - if (/Object\.keys\([^)]*(?:result|change)/.test(projectionSuiteSource)) { - fail("v2 conformance suite: exact result or change keys must not be asserted."); - } - - if (!/from "@interactive-os\/json-document"/.test(projectionReferenceSource)) { - fail("v2 reference binding: must import from the public root package."); - } - if ( - /from ["'][^"']*(?:\/src\/|domain\/|foundation\/)|\/src\//.test( - projectionReferenceSource, - ) - ) { - fail("v2 reference binding: must not import implementation-private modules."); - } - for (const [label, pattern] of [ - ["public factory", /createJSONDocument/], - ["injected suite", /runProjectionConformance/], - ["direct structural binding", /return createJSONDocument\(/], - ]) { - requirePattern(`v2 Projection binding ${label}`, projectionReferenceSource, pattern); - } - if (/\.(?:lastPatch|patch|find|canFind|canQuery)\b/.test(projectionReferenceSource)) { - fail("v2 reference binding: compatibility members leaked into the Projection adapter."); - } - - if ( - /@interactive-os\/json-document|from\s+["']zod["']|\/src\//.test( - protocolSuiteSource, - ) - ) { - fail("v2 Protocol suite: generic runner must be implementation independent."); - } - for (const [label, pattern] of [ - ["injected harness", /ProtocolHarness[\s\S]*runProtocolConformance/], - ["required-field matching", /toMatchObject/], - ["partial canonical-field check", /hasOwnProperty/], - ]) { - requirePattern(`v2 Protocol suite ${label}`, protocolSuiteSource, pattern); - } - if ( - !/from "@interactive-os\/json-document"/.test(protocolReferenceSource) - || /\/src\/|domain\/|foundation\//.test(protocolReferenceSource) - ) { - fail("v2 Protocol binding must import only the public root package."); - } - requirePattern( - "v2 Protocol binding", - protocolReferenceSource, - /applyPatch[\s\S]*runProtocolConformance/, - ); - - if ( - pointerVectors.formatVersion !== 1 - || pointerVectors.status !== "candidate" - || pointerVectors.profile !== "docs/standard/v2-projection-profile.md" - || JSON.stringify(pointerVectors.requirements) - !== JSON.stringify(["JD2-ADDRESS-001", "JD2-PATCH-001"]) - ) { - fail("v2 Pointer vectors: metadata drifted."); - } - for (const group of ["parse", "invalid", "build", "append", "parent", "track"]) { - if (!Array.isArray(pointerVectors[group]) || pointerVectors[group].length === 0) { - fail(`v2 Pointer vectors: ${group} must be a non-empty array.`); - } - } - if ( - /@interactive-os\/json-document|\/src\//.test(pointerSuiteSource) - || !/PointerHarness[\s\S]*runPointerConformance/.test(pointerSuiteSource) - ) { - fail("v2 Pointer suite must be an implementation-independent injected harness."); +const nonRuntimeRequirements = new Map(); +const nonRuntimeCounts = { static: 0, deferred: 0 }; +for (const entry of projectionVectors.nonRuntimeRequirements ?? []) { + if (!requirementSet.has(entry.id)) { + fail(`v2 vectors: unknown non-runtime requirement ${entry.id}.`); } - if ( - !/from "@interactive-os\/json-document"/.test(pointerReferenceSource) - || /\/src\/|domain\/|foundation\//.test(pointerReferenceSource) - ) { - fail("v2 Pointer binding must import only the public root package."); + if (runtimeRequirements.has(entry.id) || nonRuntimeRequirements.has(entry.id)) { + fail(`v2 vectors: duplicate requirement coverage ${entry.id}.`); } - for (const symbol of [ - "appendSegment", - "buildPointer", - "parentPointer", - "parsePointer", - "trackPointer", - "tryParsePointer", - ]) { - requirePattern( - `v2 Pointer binding ${symbol}`, - pointerReferenceSource, - new RegExp(`\\b${symbol}\\b`), - ); + if (!(entry.mode in nonRuntimeCounts)) { + fail(`v2 vectors: invalid coverage mode ${entry.mode}.`); + } else { + nonRuntimeCounts[entry.mode] += 1; } - - if ( - /@interactive-os\/json-document|\/src\//.test(rfc6902SuiteSource) - || !/RFC6902Harness[\s\S]*runRFC6902Conformance/.test(rfc6902SuiteSource) - ) { - fail("v2 RFC 6902 suite must be an implementation-independent injected harness."); - } - if ( - !/from "@interactive-os\/json-document"/.test(rfc6902ReferenceSource) - || /\/src\/|domain\/|foundation\//.test(rfc6902ReferenceSource) - ) { - fail("v2 RFC 6902 binding must import only the public root package."); + if (typeof entry.reason !== "string" || entry.reason.trim() === "") { + fail(`v2 vectors: ${entry.id} needs a reason.`); } - requirePattern( - "v2 RFC 6902 binding", - rfc6902ReferenceSource, - /applyPatch[\s\S]*runRFC6902Conformance/, - ); - - if ( - /@interactive-os\/json-document|\/src\//.test(jsonPathSuiteSource) - || !/JSONPathHarness[\s\S]*runJSONPathConformance/.test(jsonPathSuiteSource) - ) { - fail("v2 RFC 9535 suite must be an implementation-independent injected harness."); - } - if ( - !/from "@interactive-os\/json-document"/.test(jsonPathReferenceSource) - || /\/src\/|domain\/|foundation\//.test(jsonPathReferenceSource) - ) { - fail("v2 RFC 9535 binding must import only the public root package."); - } - requirePattern( - "v2 RFC 9535 binding", - jsonPathReferenceSource, - /createJSONDocument[\s\S]*runJSONPathConformance/, - ); + nonRuntimeRequirements.set(entry.id, entry.mode); } - -function validateV2PublicSurface( - draft, - profile, - contract, - baselineSignatureSource, - v2SignatureSource, +setEqual( + "v2 requirement coverage", + [...runtimeRequirements, ...nonRuntimeRequirements.keys()], + manifest.requirements, +); +equal("v2 coverage", projectionVectors.coverage, { + runtime: runtimeRequirements.size, + static: nonRuntimeCounts.static, + deferred: nonRuntimeCounts.deferred, +}); +if (/"_zod"\s*:|"safeParse"\s*:/.test(JSON.stringify([ projectionVectors, - projectionSuiteSource, - projectionReferenceSource, protocolVectors, - protocolSuiteSource, - protocolReferenceSource, - pointerVectors, - pointerSuiteSource, - pointerReferenceSource, - rfc6902SuiteSource, - rfc6902ReferenceSource, - jsonPathSuiteSource, - jsonPathReferenceSource, - packageVersion, -) { - if (draft.formatVersion !== 1) { - fail("v2 public surface: formatVersion must be 1."); - } - if (draft.status !== "candidate") { - fail("v2 public surface: status must be candidate before the stability gate passes."); - } - if (draft.sourceContract !== "packages/json-document/public-contract.json#root") { - fail("v2 public surface: sourceContract must point to the v2 root contract."); - } - if ( - draft.migrationBaselineContract - !== "packages/json-document/public-contract.json#session" - ) { - fail( - "v2 public surface: migrationBaselineContract must point to the 1.x Session baseline.", - ); - } - - const requirementIds = Array.isArray(draft.requirements) ? draft.requirements : []; - const requirementSet = new Set(requirementIds); - if (requirementIds.length !== requirementSet.size) { - fail("v2 public surface: requirement IDs must be unique."); - } - for (const requirementId of requirementIds) { - if (!/^JD2-[A-Z]+-\d{3}$/.test(requirementId)) { - fail(`v2 public surface: invalid requirement ID ${requirementId}.`); - } - } - const documentedRequirementIds = [ - ...profile.matchAll(/\bJD2-[A-Z]+-\d{3}\b/g), - ].map((match) => match[0]); - const documentedRequirementSet = new Set(documentedRequirementIds); - if (documentedRequirementIds.length !== documentedRequirementSet.size) { - fail("v2 projection profile: each requirement ID must appear exactly once."); - } - compareSets("v2 projection requirements", requirementSet, documentedRequirementSet); - - if ( - JSON.stringify(draft.projectionMembers) - !== JSON.stringify(V2_PROJECTION_MEMBERS) - ) { - fail( - `v2 public surface: projectionMembers must be exactly ${V2_PROJECTION_MEMBERS.join(", ")}.`, - ); - } - - const baselineContract = { - root: contract.session, - react: contract.react, - }; - const actualSymbols = new Set(); - for (const [entrypoint, entry] of Object.entries(baselineContract)) { - for (const kind of ["values", "types"]) { - for (const name of entry[kind] ?? []) { - const key = publicSymbolKey(entrypoint, kind, name); - if (actualSymbols.has(key)) fail(`public contract: duplicate ${key}.`); - actualSymbols.add(key); - } - } - } - if (draft.counts?.baselinePackageSymbols !== actualSymbols.size) { - fail( - `v2 public surface: expected ${draft.counts?.baselinePackageSymbols} baseline package symbols, found ${actualSymbols.size}.`, - ); - } - - const classifiedSymbols = new Map(); - const contractEntrypoints = new Set(Object.keys(baselineContract)); - compareSets( - "v2 public surface dispositions", - new Set(V2_DISPOSITIONS), - new Set(Object.keys(draft.dispositions ?? {})), - ); - for (const disposition of V2_DISPOSITIONS) { - const dispositionEntries = draft.dispositions?.[disposition]; - if (dispositionEntries === undefined) { - fail(`v2 public surface: missing ${disposition} disposition.`); - continue; - } - compareSets( - `v2 public surface ${disposition} entrypoints`, - contractEntrypoints, - new Set(Object.keys(dispositionEntries)), - ); - - let dispositionCount = 0; - for (const entrypoint of contractEntrypoints) { - const entry = dispositionEntries[entrypoint]; - if (entry === undefined) continue; - compareSets( - `v2 public surface ${disposition}/${entrypoint} kinds`, - new Set(["values", "types"]), - new Set(Object.keys(entry)), - ); - for (const kind of ["values", "types"]) { - const names = entry[kind]; - if (!Array.isArray(names)) { - fail(`v2 public surface: ${disposition}/${entrypoint}/${kind} must be an array.`); - continue; - } - for (const name of names) { - const key = publicSymbolKey(entrypoint, kind, name); - if (classifiedSymbols.has(key)) { - fail(`v2 public surface: ${key} is classified more than once.`); - } - classifiedSymbols.set(key, disposition); - dispositionCount += 1; - } - } - } - if (draft.counts?.dispositions?.[disposition] !== dispositionCount) { - fail( - `v2 public surface: ${disposition} count is ${dispositionCount}, not ${draft.counts?.dispositions?.[disposition]}.`, - ); - } - } - compareSets( - "v2 public surface package symbols", - actualSymbols, - new Set(classifiedSymbols.keys()), - ); - - const actualDocumentMembers = extractExpectedDocumentMembers( - baselineSignatureSource, - ); - if (draft.counts?.baselineDocumentMembers !== actualDocumentMembers.size) { - fail( - `v2 public surface: expected ${draft.counts?.baselineDocumentMembers} baseline document members, found ${actualDocumentMembers.size}.`, - ); - } - const classifiedDocumentMembers = new Map(); - compareSets( - "v2 public surface document dispositions", - new Set(V2_DISPOSITIONS), - new Set(Object.keys(draft.documentMembers ?? {})), - ); - for (const disposition of V2_DISPOSITIONS) { - const members = draft.documentMembers?.[disposition]; - if (!Array.isArray(members)) { - fail(`v2 public surface: document ${disposition} members must be an array.`); - continue; - } - for (const member of members) { - if (classifiedDocumentMembers.has(member)) { - fail(`v2 public surface: document member ${member} is classified more than once.`); - } - classifiedDocumentMembers.set(member, disposition); - } - if (draft.counts?.documentDispositions?.[disposition] !== members.length) { - fail( - `v2 public surface: document ${disposition} count is ${members.length}, not ${draft.counts?.documentDispositions?.[disposition]}.`, - ); - } - } - compareSets( - "v2 public surface document members", - actualDocumentMembers, - new Set(classifiedDocumentMembers.keys()), - ); - if ( - JSON.stringify(draft.documentMembers?.kernel) - !== JSON.stringify(V2_PROJECTION_MEMBERS) - ) { - fail("v2 public surface: document kernel must be the ordered six-member Projection."); - } - - const packageCompat = new Set( - [...classifiedSymbols.entries()] - .filter(([, disposition]) => disposition === "compat") - .map(([key]) => key), - ); - const compatReplacements = draft.compatReplacements ?? {}; - compareSets( - "v2 public surface package compat replacements", - packageCompat, - new Set(Object.keys(compatReplacements)), - ); - for (const [key, rule] of Object.entries(compatReplacements)) { - if ( - typeof rule?.replacement !== "string" - || rule.replacement.trim() === "" - || typeof rule?.reason !== "string" - || rule.reason.trim() === "" - ) { - fail(`v2 public surface: ${key} needs a non-empty replacement and reason.`); - } - } - - const documentCompat = new Set( - [...classifiedDocumentMembers.entries()] - .filter(([, disposition]) => disposition === "compat") - .map(([name]) => name), - ); - const documentCompatReplacements = draft.documentCompatReplacements ?? {}; - compareSets( - "v2 public surface document compat replacements", - documentCompat, - new Set(Object.keys(documentCompatReplacements)), - ); - for (const [name, replacement] of Object.entries(documentCompatReplacements)) { - if (typeof replacement !== "string" || replacement.trim() === "") { - fail(`v2 public surface: document ${name} needs a non-empty replacement.`); - } - } - - const introducedSymbols = new Set(); - for (const symbol of draft.introducedSymbols ?? []) { - const key = publicSymbolKey(symbol.entrypoint, symbol.kind, symbol.name); - if (!contractEntrypoints.has(symbol.entrypoint)) { - fail(`v2 public surface: introduced symbol has unknown entrypoint ${key}.`); - } - if (!["values", "types"].includes(symbol.kind)) { - fail(`v2 public surface: introduced symbol has invalid kind ${key}.`); - } - if (!["kernel", "session"].includes(symbol.disposition)) { - fail(`v2 public surface: introduced symbol has invalid disposition ${key}.`); - } - if (introducedSymbols.has(key)) { - fail(`v2 public surface: introduced symbol appears more than once ${key}.`); - } - introducedSymbols.add(key); - if (typeof symbol.implemented !== "boolean") { - fail(`v2 public surface: introduced symbol needs implemented boolean ${key}.`); - } - const rootBindingHasSymbol = (contract.root?.[symbol.kind] ?? []) - .includes(symbol.name); - if (symbol.implemented !== rootBindingHasSymbol) { - fail(`v2 public surface: implemented state drifted for ${key}.`); - } - for (const replaced of symbol.replaces ?? []) { - const isCurrentSymbol = actualSymbols.has(replaced); - const isDocumentMember = replaced.startsWith("document:") - && actualDocumentMembers.has(replaced.slice("document:".length)); - if (!isCurrentSymbol && !isDocumentMember) { - fail(`v2 public surface: introduced symbol ${key} replaces unknown ${replaced}.`); - } - } - } - - for (const [source, rule] of Object.entries(compatReplacements)) { - const targets = [ - ...rule.replacement.matchAll( - /\b(root|react|proposed|introduced):(values|types):([A-Za-z_$][\w$]*)\b/g, - ), - ]; - for (const [, entrypoint, kind, name] of targets) { - const target = ["proposed", "introduced"].includes(entrypoint) - ? publicSymbolKey("root", kind, name) - : publicSymbolKey(entrypoint, kind, name); - const exists = ["proposed", "introduced"].includes(entrypoint) - ? introducedSymbols.has(target) - : actualSymbols.has(target); - if (!exists) { - fail(`v2 public surface: ${source} references unknown replacement ${target}.`); - } - if ( - !["proposed", "introduced"].includes(entrypoint) - && !["kernel", "session"].includes(classifiedSymbols.get(target)) - ) { - fail(`v2 public surface: ${source} points to non-canonical replacement ${target}.`); - } - } - } - - const dispositionLabels = { - kernel: "Kernel", - session: "Session", - compat: "Compat", - remove: "Remove", - }; - for (const disposition of V2_DISPOSITIONS) { - const count = draft.counts?.dispositions?.[disposition]; - requirePattern( - "v2 projection profile disposition summary", - profile, - new RegExp(`\\| ${dispositionLabels[disposition]} \\| ${count} \\|`), - ); - } - - if ( - draft.binding?.version !== packageVersion - || draft.binding?.contract !== "packages/json-document/public-contract.json#root" - || draft.binding?.entrypoint !== "." - || draft.binding?.sessionEntrypoint !== "./session" - ) { - fail("v2 public surface: package binding metadata drifted."); - } - compareSets( - "v2 root value binding", - new Set(draft.binding?.values ?? []), - new Set(contract.root?.values ?? []), - ); - compareSets( - "v2 root type binding", - new Set(draft.binding?.types ?? []), - new Set(contract.root?.types ?? []), - ); - requirePattern( - "v2 signature exact six-member surface", - v2SignatureSource, - /ProjectionMembers[\s\S]*"value" \| "at" \| "query" \| "canPatch" \| "commit" \| "subscribe"/, - ); - requirePattern( - "v2 signature untyped JSON boundary", - v2SignatureSource, - /document\.value satisfies JSONValue/, - ); - if (/JSONDocument|document\.value\.id/.test(v2SignatureSource)) { - fail("v2 signature: unsound shape-preserving document generic returned."); - } - - validateV2Conformance( - draft, - projectionVectors, - projectionSuiteSource, - projectionReferenceSource, - protocolVectors, - protocolSuiteSource, - protocolReferenceSource, - pointerVectors, - pointerSuiteSource, - pointerReferenceSource, - rfc6902SuiteSource, - rfc6902ReferenceSource, - jsonPathSuiteSource, - jsonPathReferenceSource, - requirementSet, - ); +]))) { + fail("v2 vectors: provider object leaked."); } -const standard = read("docs/standard/core-standard.md"); -const conformanceProfile = read("docs/standard/conformance-profile.md"); -const foundationGate = read("docs/standard/foundation-gate.md"); -const resultContract = read("docs/standard/result-contract.md"); -const selectionContract = read("docs/standard/selection-contract.md"); -const schemaIntrospectionContract = read("docs/standard/schema-introspection-contract.md"); -const v2ProjectionProfile = read("docs/standard/v2-projection-profile.md"); -const v2PublicSurface = JSON.parse(read("docs/standard/v2-public-surface.json")); -const v2ProjectionVectors = JSON.parse( - read(v2PublicSurface.conformance.projectionVectors), -); -const v2ProjectionSuite = read(v2PublicSurface.conformance.projectionSuite); -const v2ProjectionReference = read(v2PublicSurface.conformance.projectionBinding); -const v2ProtocolVectors = JSON.parse( - read(v2PublicSurface.conformance.protocolVectors), -); -const v2ProtocolSuite = read(v2PublicSurface.conformance.protocolSuite); -const v2ProtocolReference = read(v2PublicSurface.conformance.protocolBinding); -const v2PointerVectors = JSON.parse( - read(v2PublicSurface.conformance.pointerVectors), +assertGenericSuite( + "projection suite", + projectionSuite, + /ProjectionHarness[\s\S]*runProjectionConformance/, ); -const v2PointerSuite = read(v2PublicSurface.conformance.pointerSuite); -const v2PointerReference = read(v2PublicSurface.conformance.pointerBinding); -const v2RFC6902Suite = read(v2PublicSurface.conformance.rfc6902Suite); -const v2RFC6902Reference = read(v2PublicSurface.conformance.rfc6902Binding); -const v2JSONPathSuite = read(v2PublicSurface.conformance.jsonPathSuite); -const v2JSONPathReference = read(v2PublicSurface.conformance.jsonPathBinding); -const v2SignatureContract = read( - "packages/json-document/tests/public/v2-signature-contract.test-d.ts", -); -const conformance = read("packages/json-document/tests/public/standard-conformance.test.ts"); -const semanticContract = read("packages/json-document/tests/public/semantic-contract.test.ts"); -const signatureContract = read("packages/json-document/tests/public/signature-contract.test-d.ts"); -const publicContract = JSON.parse(read("packages/json-document/public-contract.json")); -const rootPackage = JSON.parse(read("package.json")); -const libraryPackage = JSON.parse(read("packages/json-document/package.json")); - -for (const [label, pattern] of [ - ["normative language", /\bMUST\b[\s\S]*\bSHOULD\b[\s\S]*\bMAY\b/], - ["conformance classes", /## 3\. 적합성 등급/], - ["data model", /## 4\. 데이터 모델/], - ["pointer-query-mutation distinction", /JSONPath는 mutation target으로 받아들이면 안 된다/], - ["schema semantics", /## 6\. Schema 의미론/], - ["document surface", /find[\s\S]*insert[\s\S]*replace[\s\S]*delete[\s\S]*move[\s\S]*duplicate[\s\S]*copy[\s\S]*cut[\s\S]*paste[\s\S]*undo[\s\S]*redo[\s\S]*canFind[\s\S]*canInsert[\s\S]*canRedo/], - ["strict semantics", /기본값은 `strict: false`/], - ["selection semantics", /selection은 DOM focus가 아니라 headless document data다/], - ["semantic fixture lock", /Semantic fixture[\s\S]*result code[\s\S]*strict 기본값[\s\S]*실패 atomicity/], - ["signature fixture lock", /Signature fixture[\s\S]*overload[\s\S]*call shape/], - ["clipboard spread", /직접 array payload를 `insert\(target, payload, \{ spread: true \}\)`로 넣으면[\s\S]*item별 sibling insert/], - ["history semantics", /history는 undo\/redo control surface/], - ["breaking change", /breaking change로\s+취급해야 한다/], - ["adapter pressure", /form editing[\s\S]*storage, history, collaboration bridge/], - ["conformance", /적합성 suite는 public package entrypoint에서만 import해야 한다/], +for (const pattern of [ + /readonly value[\s\S]*at\([\s\S]*query\([\s\S]*canPatch\([\s\S]*commit\([\s\S]*subscribe\(/, + /toMatchObject/, ]) { - requirePattern("core standard", standard, pattern); + requirePattern("projection suite", projectionSuite, pattern); } - -for (const [label, pattern] of [ - ["documented profile", /1\.0의 conformance profile은 문서화된 profile/], - ["no runner package", /별도 npm package[\s\S]*CLI runner[\s\S]*1\.0 범위에 포함하지 않는다/], - ["public entrypoint", /public package entrypoint/], - ["profile artifacts", /public-contract\.json[\s\S]*signature-contract\.test-d\.ts[\s\S]*semantic-contract\.test\.ts[\s\S]*result-contract\.test\.ts[\s\S]*standard-conformance\.test\.ts/], - ["included semantics", /JSON Pointer[\s\S]*JSONPath[\s\S]*selectionAfter[\s\S]*clipboard[\s\S]*undo\/redo/], - ["excluded surfaces", /official extension[\s\S]*lab extension[\s\S]*DOM focus/], - ["breaking criteria", /breaking change[\s\S]*public export[\s\S]*error code[\s\S]*selectionAfter/], -]) { - requirePattern("conformance profile", conformanceProfile, pattern); +if (/Object\.keys\([^)]*(?:result|change)/.test(projectionSuite)) { + fail("projection suite: exact result or change keys must not be asserted."); } - -for (const token of [ - "src/index.ts", - "src/react.ts", - "application/document", - "domain/schema", - "domain/selection", - "foundation/patch", - "foundation/json", - "foundation/pointer", -]) { - if (standard.includes(token)) fail(`core standard: internal source path leaked: ${token}.`); -} - -for (const [label, pattern] of [ - ["foundation tree", /RFC급 foundation/], - ["normative artifact", /core-standard\.md/], - ["conformance profile artifact", /conformance-profile\.md/], - ["result freeze artifact", /result-contract\.md/], - ["selection freeze artifact", /selection-contract\.md/], - ["schema freeze artifact", /schema-introspection-contract\.md/], - ["conformance artifact", /standard-conformance\.test\.ts/], - ["semantic fixture artifact", /semantic-contract\.test\.ts/], - ["signature fixture artifact", /signature-contract\.test-d\.ts/], - ["evaluator artifact", /evaluate-standardization\.mjs/], - ["adapter pressure", /form[\s\S]*table\/data-grid[\s\S]*outliner\/tree[\s\S]*rich text[\s\S]*storage\/collaboration/i], -]) { - requirePattern("foundation gate", foundationGate, pattern); -} - -for (const [label, pattern] of [ - ["json result shape", /## JSONResult[\s\S]*invalid_pointer[\s\S]*schema_violation/], - ["capability result shape", /## JSONCapabilityResult[\s\S]*CapabilityErrorCode[\s\S]*violations\[\]\.path/], - ["preflight and clipboard codes", /preflight_failed[\s\S]*empty_clipboard/], - ["schema violation path modes", /schema-slot[\s\S]*document-result/], - ["breaking changes", /## Breaking Change[\s\S]*error code/], -]) { - requirePattern("result contract", resultContract, pattern); -} - -for (const [label, pattern] of [ - ["selection mode", /## SelectionMode[\s\S]*`single`[\s\S]*`multiple`[\s\S]*`extended`/], - ["selection snap", /## SelectionSnap[\s\S]*selectedPointers[\s\S]*selectionRanges[\s\S]*primaryIndex/], - ["selection after", /selectionAfter[\s\S]*history entry/], - ["text edit codes", /missing_length[\s\S]*multi_pointer_range[\s\S]*overlapping_ranges[\s\S]*not_string/], - ["non-goals", /DOM focus[\s\S]*2D marquee[\s\S]*stable object id resolver/], -]) { - requirePattern("selection contract", selectionContract, pattern); -} - -for (const [label, pattern] of [ - ["schema state", /## SchemaState[\s\S]*accepts/], - ["path mode", /SchemaPathMode[\s\S]*`value`[\s\S]*`insert`/], - ["schema kind", /SchemaKind[\s\S]*discriminatedUnion[\s\S]*nullable/], - ["description", /SchemaDescription[\s\S]*jsonSchema[\s\S]*discriminator/], - ["schema slot result", /schema-slot[\s\S]*document-result/], - ["capability result", /JSONCapabilityResult[\s\S]*schema_violation/], -]) { - requirePattern("schema introspection contract", schemaIntrospectionContract, pattern); +assertPublicBinding( + "projection binding", + projectionBinding, + /createJSONDocument[\s\S]*runProjectionConformance/, +); +requirePattern( + "projection binding", + projectionBinding, + /return createJSONDocument\(/, +); +if (/\.(?:lastPatch|patch|find|canFind|canQuery)\b/.test(projectionBinding)) { + fail("projection binding: archived compatibility member leaked."); } - -if (!/from "@interactive-os\/json-document\/session"/.test(conformance)) { - fail("1.x standard conformance: must import from the Session baseline."); -} -if (/from "\.\.|from '\.\.|\/src\//.test(conformance)) { - fail("standard conformance: must not import implementation-private modules."); -} -for (const [label, pattern] of [ - ["jsonpath query", /query\("\$\.columns\[\*\]\.cards\[\*\]\.id"\)/], - ["json pointer mutation", /path: "\/columns\/0\/cards\/0\/title"/], - ["capability purity", /keeps capability probes reasoned and mutation-free/], - ["selection history", /commits patch and final selection as one history step/], - ["clipboard insert split", /uses insert for explicit payloads/], - ["subscriber atomicity", /only after successful atomic changes/], -]) { - requirePattern("standard conformance", conformance, pattern); +assertGenericSuite( + "protocol suite", + protocolSuite, + /ProtocolHarness[\s\S]*runProtocolConformance/, +); +for (const pattern of [/toMatchObject/, /hasOwnProperty/]) { + requirePattern("protocol suite", protocolSuite, pattern); } - -if (!/from "@interactive-os\/json-document\/session"/.test(semanticContract)) { - fail("1.x semantic contract: must import from the Session baseline."); -} -if (/from "\.\.|from '\.\.|\/src\//.test(semanticContract)) { - fail("semantic contract: must not import implementation-private modules."); -} -for (const [label, pattern] of [ - ["fixture suite", /json-document 1\.0 semantic contract fixtures/], - ["stable result codes", /invalid_pointer[\s\S]*schema_violation[\s\S]*empty_stack[\s\S]*empty_clipboard[\s\S]*not_serializable/], - ["strict policy", /strict policy[\s\S]*strict: true[\s\S]*JSONDocumentError/], - ["failed mutation atomicity", /failed mutation atomicity[\s\S]*undoDepth[\s\S]*observed\)\.toEqual\(\[\]\)/], - ["clipboard and selection history", /clipboard spread[\s\S]*selection history[\s\S]*undo\(\)[\s\S]*redo\(\)/], +assertPublicBinding( + "protocol binding", + protocolBinding, + /applyPatch[\s\S]*runProtocolConformance/, +); +assertGenericSuite( + "pointer suite", + pointerSuite, + /PointerHarness[\s\S]*runPointerConformance/, +); +assertPublicBinding( + "pointer binding", + pointerBinding, + /appendSegment[\s\S]*runPointerConformance/, +); +for (const symbol of [ + "appendSegment", + "buildPointer", + "parentPointer", + "parsePointer", + "trackPointer", + "tryParsePointer", ]) { - requirePattern("semantic contract", semanticContract, pattern); + requirePattern("pointer binding", pointerBinding, new RegExp(`\\b${symbol}\\b`)); } +assertGenericSuite( + "RFC 6902 suite", + rfc6902Suite, + /RFC6902Harness[\s\S]*runRFC6902Conformance/, +); +assertPublicBinding( + "RFC 6902 binding", + rfc6902Binding, + /applyPatch[\s\S]*runRFC6902Conformance/, +); +assertGenericSuite( + "RFC 9535 suite", + jsonPathSuite, + /JSONPathHarness[\s\S]*runJSONPathConformance/, +); +assertPublicBinding( + "RFC 9535 binding", + jsonPathBinding, + /createJSONDocument[\s\S]*runJSONPathConformance/, +); -if (!/from "@interactive-os\/json-document\/session"/.test(signatureContract)) { - fail("1.x signature contract: must import from the Session baseline."); -} -if (!/from "@interactive-os\/json-document\/react"/.test(signatureContract)) { - fail("signature contract: must import React from the public react package."); -} -if (/from "\.\.|from '\.\.|\/src\//.test(signatureContract)) { - fail("signature contract: must not import implementation-private modules."); -} -for (const [label, pattern] of [ - ["fixture type", /signature-contract\.test-d\.ts/], - ["create overload", /ExpectedCreateJSONDocument[\s\S]*TrustedInitialDocumentOptions[\s\S]*UntrustedInitialDocumentOptions/], - ["react overload", /ExpectedUseJSONDocument[\s\S]*TrustedInitialDocumentOptions[\s\S]*UntrustedInitialDocumentOptions/], - ["document surface", /ExpectedJSONDocument[\s\S]*insert\(target[\s\S]*move\(source[\s\S]*paste\(target[\s\S]*canPaste\(target/], - ["patch helpers", /ExpectedPatchHelpers[\s\S]*applyOperation[\s\S]*applyPatchToTrustedState/], - ["placement shapes", /JSONDocumentPasteTarget[\s\S]*before[\s\S]*after[\s\S]*into[\s\S]*replace/], - ["negative shape", /@ts-expect-error[\s\S]*\{ at \}/], +for (const pattern of [ + /Object\.keys\(packageJson\.exports\)\.join\(","\) !== "\."/, + /packageJson\.peerDependencies !== undefined/, + /application\/session/, + /application\/react-document/, + /Archived implementation leaked into dist/, ]) { - requirePattern("signature contract", signatureContract, pattern); + requirePattern("package smoke v2 closure", packageSmoke, pattern); } - -if (!publicContract.session.values.includes("createJSONDocument")) { - fail("Session baseline contract: missing createJSONDocument."); -} -if (!publicContract.react.values.includes("useJSONDocument")) { - fail("public contract: missing useJSONDocument."); -} -for (const requiredType of [ - "JSONDocument", - "JSONCapabilityResult", - "JSONDocumentInsertTarget", - "JSONDocumentInsertOptions", - "JSONDocumentMoveTarget", - "SelectionSnap", - "ClipboardPasteResult", - "JSONDocumentHistory", +for (const pattern of [ + /dist\/application\/document\/index\.js/, + /createJSONDocument/, + /applyPatch/, + /commit single leaf replace/, ]) { - if (!publicContract.session.types.includes(requiredType)) { - fail(`Session baseline contract: missing ${requiredType}.`); - } + requirePattern("v2 core benchmark", coreBenchmark, pattern); } - -validateV2PublicSurface( - v2PublicSurface, - v2ProjectionProfile, - publicContract, - signatureContract, - v2SignatureContract, - v2ProjectionVectors, - v2ProjectionSuite, - v2ProjectionReference, - v2ProtocolVectors, - v2ProtocolSuite, - v2ProtocolReference, - v2PointerVectors, - v2PointerSuite, - v2PointerReference, - v2RFC6902Suite, - v2RFC6902Reference, - v2JSONPathSuite, - v2JSONPathReference, - libraryPackage.version, -); - -if (!rootPackage.scripts?.["standard:check"]) { - fail("package scripts: missing standard:check."); -} -if (!rootPackage.scripts?.["release:check"]?.includes("standard:check")) { - fail("package scripts: release:check must include standard:check."); +if (/application\/session|from\s+["']zod["']/.test(coreBenchmark)) { + fail("v2 core benchmark: archived session or schema provider leaked."); } -if (!libraryPackage.files?.includes("public-contract.json")) { - fail("json-document package: public-contract.json must be published with the package."); +if (failures.length > 0) { + console.error( + `json-document v2 standardization failed:\n${failures.map((failure) => `- ${failure}`).join("\n")}`, + ); + process.exitCode = 1; +} else { + console.log( + "json-document v2 standardization ok: 1 entrypoint, 20 exports, 6 document members, 0 runtime peers", + ); } From b20ff5fb7cd2b485bcd709d2bf06d09e2959a486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Sun, 26 Jul 2026 17:43:24 +0900 Subject: [PATCH 3/4] refactor: isolate v1 from the v2 release graph --- .github/workflows/ci.yml | 34 +- .github/workflows/lab-extensions.yml | 10 +- .github/workflows/publish.yml | 79 - README.md | 37 +- apps/site/package.json | 6 +- apps/site/src/App.tsx | 13 +- apps/site/src/generated/repo-catalog.ts | 3261 --------------- .../InterfaceWorkbench.playground.tsx | 1909 --------- apps/site/src/routes/Docs.tsx | 23 - apps/site/src/routes/Home.tsx | 3 - apps/site/src/routes/Playground.tsx | 9 - apps/site/src/site-routes.json | 35 - apps/site/tailwind.config.cjs | 7 - apps/site/tests/docs-consistency.test.ts | 165 +- apps/site/tests/interface-workbench.test.tsx | 378 -- apps/site/tests/site-shell.test.tsx | 19 +- apps/site/tsconfig.json | 1 - apps/site/vite.config.ts | 4 +- archive/v1/README.md | 16 + .../v1/apps}/copy-review-lab/README.md | 0 .../v1/apps}/copy-review-lab/index.html | 0 .../v1/apps}/copy-review-lab/package.json | 0 .../v1/apps}/copy-review-lab/src/App.tsx | 0 .../copy-review-lab/src/copy-review-lab.css | 0 .../v1/apps}/copy-review-lab/src/main.tsx | 0 .../tests/copy-review-lab.test.tsx | 0 .../v1/apps}/copy-review-lab/tsconfig.json | 0 .../v1/apps}/copy-review-lab/vite.config.ts | 0 .../v1/apps}/grouping-lab/README.md | 0 .../v1/apps}/grouping-lab/index.html | 0 .../v1/apps}/grouping-lab/package.json | 0 .../v1/apps}/grouping-lab/src/App.tsx | 0 .../apps}/grouping-lab/src/grouping-lab.css | 0 .../v1/apps}/grouping-lab/src/main.tsx | 0 .../grouping-lab/tests/grouping-lab.test.tsx | 0 .../v1/apps}/grouping-lab/tsconfig.json | 0 .../v1/apps}/grouping-lab/vite.config.ts | 0 .../v1/apps}/mobile-cms/package.json | 0 .../v1/apps}/mobile-cms/src/App.tsx | 0 .../v1/apps}/mobile-cms/src/mobile-cms.css | 0 .../mobile-cms/tests/mobile-cms-ux.test.tsx | 0 .../v1/apps}/mobile-cms/tsconfig.json | 0 .../v1/apps}/mobile-cms/vitest.config.ts | 0 .../v1/apps}/outliner-blind-a/index.html | 0 .../v1/apps}/outliner-blind-a/package.json | 0 .../v1/apps}/outliner-blind-a/src/App.tsx | 0 .../v1/apps}/outliner-blind-a/src/main.tsx | 0 .../v1/apps}/outliner-blind-a/src/outline.ts | 0 .../v1/apps}/outliner-blind-a/src/schema.ts | 0 .../v1/apps}/outliner-blind-a/src/styles.css | 0 .../tests/outliner-blind-a.test.tsx | 0 .../v1/apps}/outliner-blind-a/tsconfig.json | 0 .../v1/apps}/outliner-blind-a/vite.config.ts | 0 .../apps}/outliner-blind-a/vitest.config.ts | 0 .../v1/apps}/outliner-blind-b/index.html | 0 .../v1/apps}/outliner-blind-b/package.json | 0 .../v1/apps}/outliner-blind-b/src/App.tsx | 0 .../v1/apps}/outliner-blind-b/src/main.tsx | 0 .../src/outlinerModel.test.ts | 0 .../outliner-blind-b/src/outlinerModel.ts | 0 .../v1/apps}/outliner-blind-b/src/styles.css | 0 .../v1/apps}/outliner-blind-b/tsconfig.json | 0 .../v1/apps}/outliner-blind-b/vite.config.ts | 0 .../apps}/outliner-blind-b/vitest.config.ts | 0 .../v1/apps}/outliner/package.json | 0 .../v1/apps}/outliner/src/OutlineRow.tsx | 0 .../v1/apps}/outliner/src/Outliner.tsx | 0 .../v1/apps}/outliner/src/clipboard.ts | 0 .../apps}/outliner/src/commands/clipboard.ts | 0 .../v1/apps}/outliner/src/commands/context.ts | 0 .../v1/apps}/outliner/src/commands/focus.ts | 0 .../v1/apps}/outliner/src/commands/index.ts | 0 .../apps}/outliner/src/commands/selection.ts | 0 .../apps}/outliner/src/commands/structure.ts | 0 .../outliner/src/hooks/useClickPolicy.ts | 0 .../apps}/outliner/src/hooks/useDispatch.ts | 0 .../apps}/outliner/src/hooks/useDraftState.ts | 0 .../apps}/outliner/src/hooks/useGlobalKey.ts | 0 .../outliner/src/hooks/useTextEditCoalesce.ts | 0 .../v1/apps}/outliner/src/hooks/useToasts.ts | 0 .../v1/apps}/outliner/src/index.ts | 0 .../v1/apps}/outliner/src/keymap.ts | 0 .../v1/apps}/outliner/src/outliner.css | 0 .../v1/apps}/outliner/src/pointer-utils.ts | 0 .../v1/apps}/outliner/src/schema.ts | 0 .../tests/coverage-keyboard-mouse.test.tsx | 0 .../outliner/tests/outliner-history.test.tsx | 0 .../tests/outliner-interactions.test.tsx | 0 .../tests/selection-auto-rules.test.tsx | 0 .../v1/apps}/outliner/tests/stress.test.tsx | 0 .../v1/apps}/outliner/tsconfig.json | 0 .../v1/apps}/outliner/vitest.config.ts | 0 .../v1/apps}/proposed-changes-lab/README.md | 0 .../v1/apps}/proposed-changes-lab/index.html | 0 .../apps}/proposed-changes-lab/package.json | 0 .../v1/apps}/proposed-changes-lab/src/App.tsx | 0 .../apps}/proposed-changes-lab/src/main.tsx | 0 .../src/proposed-changes-lab.css | 0 .../tests/proposed-changes-lab.test.tsx | 0 .../apps}/proposed-changes-lab/tsconfig.json | 0 .../apps}/proposed-changes-lab/vite.config.ts | 0 .../v1/apps}/protected-ranges-lab/README.md | 0 .../v1/apps}/protected-ranges-lab/index.html | 0 .../apps}/protected-ranges-lab/package.json | 0 .../v1/apps}/protected-ranges-lab/src/App.tsx | 0 .../apps}/protected-ranges-lab/src/main.tsx | 0 .../src/protected-ranges-lab.css | 0 .../tests/protected-ranges-lab.test.tsx | 0 .../apps}/protected-ranges-lab/tsconfig.json | 0 .../apps}/protected-ranges-lab/vite.config.ts | 0 .../v1/apps}/review-comments-lab/README.md | 0 .../v1/apps}/review-comments-lab/index.html | 0 .../v1/apps}/review-comments-lab/package.json | 0 .../v1/apps}/review-comments-lab/src/App.tsx | 0 .../v1/apps}/review-comments-lab/src/main.tsx | 0 .../src/review-comments-lab.css | 0 .../tests/review-comments-lab.test.tsx | 0 .../apps}/review-comments-lab/tsconfig.json | 0 .../apps}/review-comments-lab/vite.config.ts | 0 .../v1/apps}/schema-form-lab/index.html | 0 .../v1/apps}/schema-form-lab/package.json | 0 .../v1/apps}/schema-form-lab/src/App.tsx | 0 .../v1/apps}/schema-form-lab/src/main.tsx | 0 .../schema-form-lab/src/schema-form-lab.css | 0 .../tests/schema-form-lab.test.tsx | 0 .../v1/apps}/schema-form-lab/tsconfig.json | 0 .../v1/apps}/schema-form-lab/vite.config.ts | 0 .../v1/apps}/snippet-composer-lab/README.md | 0 .../v1/apps}/snippet-composer-lab/index.html | 0 .../apps}/snippet-composer-lab/package.json | 0 .../v1/apps}/snippet-composer-lab/src/App.tsx | 0 .../apps}/snippet-composer-lab/src/main.tsx | 0 .../src/snippet-composer-lab.css | 0 .../tests/snippet-composer-lab.test.tsx | 0 .../apps}/snippet-composer-lab/tsconfig.json | 0 .../apps}/snippet-composer-lab/vite.config.ts | 0 archive/v1/docs/changelog.md | 314 ++ .../v1/docs}/public/extensions.md | 0 {docs => archive/v1/docs}/public/recipes.md | 0 .../de-facto-editing-feature-taxonomy.md | 0 .../v1/docs}/standard/conformance-profile.md | 0 .../standard/contract-pressure-register.md | 0 .../v1/docs}/standard/core-standard.md | 0 .../standard/extension-delegation-standard.md | 0 .../v1/docs}/standard/foundation-gate.md | 0 .../v1/docs}/standard/json-document-spec.md | 0 .../v1/docs}/standard/public-api-layering.md | 0 .../v1/docs}/standard/result-contract.md | 0 .../standard/schema-introspection-contract.md | 0 .../v1/docs}/standard/selection-contract.md | 1 - .../standard/self-improvement-loop-report.md | 0 .../src/application/react-document/index.ts | 0 .../src/application/session/contract.ts | 0 .../src/application/session/create.ts | 0 .../src/application/session/index.ts | 0 .../src/domain/clipboard/copy.ts | 0 .../json-document/src/domain/clipboard/cut.ts | 0 .../src/domain/clipboard/paste.ts | 0 .../domain/document/capabilities/create.ts | 0 .../domain/document/capabilities/result.ts | 0 .../src/domain/document/clipboard/buffer.ts | 0 .../domain/document/clipboard/clipboard.ts | 0 .../src/domain/document/clipboard/contract.ts | 0 .../src/domain/document/clipboard/paste.ts | 0 .../src/domain/document/create.ts | 0 .../src/domain/document/editing/actions.ts | 0 .../src/domain/document/editing/plan.ts | 0 .../src/domain/document/editing/target.ts | 0 .../src/domain/document/history/metadata.ts | 0 .../src/domain/document/history/restore.ts | 0 .../src/domain/document/history/undoRedo.ts | 0 .../src/domain/document/index.ts | 0 .../src/domain/document/reading/read.ts | 0 .../src/domain/document/schema/accepts.ts | 0 .../src/domain/document/schema/description.ts | 0 .../src/domain/document/schema/query.ts | 0 .../src/domain/document/schema/resolve.ts | 0 .../src/domain/document/schema/state.ts | 0 .../src/domain/document/selection/create.ts | 0 .../src/domain/document/selection/runtime.ts | 0 .../src/domain/document/state/json.ts | 0 .../src/domain/document/state/ops.ts | 0 .../src/domain/document/state/ownership.ts | 0 .../src/domain/document/state/patch.ts | 0 .../src/domain/document/text-surface/index.ts | 0 .../src/domain/editing/duplicate.ts | 0 .../src/domain/schema/array/add.ts | 0 .../src/domain/schema/array/path.ts | 0 .../src/domain/schema/array/replace.ts | 0 .../domain/schema/inspection/introspection.ts | 0 .../src/domain/schema/inspection/zod.ts | 0 .../src/domain/schema/model/knownJson.ts | 0 .../src/domain/schema/model/result.ts | 0 .../src/domain/schema/model/schema.ts | 0 .../src/domain/schema/model/value.ts | 0 .../src/domain/schema/mutation/patch.ts | 0 .../src/domain/schema/mutation/rekey.ts | 0 .../src/domain/schema/object/record.ts | 0 .../src/domain/schema/object/replace.ts | 0 .../src/domain/schema/object/value.ts | 0 .../src/domain/schema/validation/patch.ts | 0 .../src/domain/schema/validation/replace.ts | 0 .../src/domain/schema/validation/schema.ts | 0 .../src/domain/selection/autoRules.ts | 0 .../src/domain/selection/order.ts | 0 .../src/domain/selection/point.ts | 0 .../src/domain/selection/read.ts | 0 .../src/domain/selection/reducer.ts | 0 .../src/domain/selection/snap.ts | 0 .../src/domain/selection/spans.ts | 0 .../src/domain/selection/textDelete.ts | 0 .../src/domain/selection/textEdit.ts | 0 .../src/domain/selection/traversal.ts | 0 .../src/domain/text-surface/surface.ts | 0 .../src/foundation/error/index.ts | 0 .../src/foundation/history/index.ts | 0 .../src/foundation/patch/index.ts | 0 .../src/foundation/patch/inverse.ts | 0 .../src/foundation/patch/schema-contract.ts | 0 .../src/foundation/patch/schema.ts | 0 .../src/foundation/patch/source.ts | 0 .../src/foundation/pointer/index.ts | 0 .../src/foundation/pointer/siblingRange.ts | 0 .../src/foundation/pointer/source.ts | 0 .../document/capability/integration.test.ts | 0 .../document/clipboard/integration.test.ts | 0 .../tests/document/clipboard/react.test.ts | 0 .../document/create-json-document.test.ts | 0 .../tests/document/history/load-react.test.ts | 0 .../tests/document/history/metadata.test.ts | 0 .../tests/document/interface.test.ts | 0 .../tests/document/read/integration.test.ts | 0 .../tests/document/read/react.test.ts | 0 .../tests/document/schema/integration.test.ts | 0 .../tests/document/schema/react.test.ts | 0 .../tests/document/selection/headless.test.ts | 0 .../tests/document/selection/react.test.ts | 0 .../tests/document/text-surface.test.ts | 0 .../tests/document/validation-paths.test.ts | 0 .../json-document/tests/public/error.test.ts | 0 .../tests/public/guarantees.test.ts | 0 .../tests/public/json-patch/apply.test.ts | 0 .../public/json-patch/conformance.test.ts | 0 .../tests/public/json-patch/rfc6902.test.ts | 0 .../public/json-pointer/sibling-range.test.ts | 0 .../public/json-pointer/uri-fragment.test.ts | 0 .../tests/public/jsonpath/cts.test.ts | 0 .../public/pointer-tracking/pointer.test.ts | 0 .../tests/public/result-contract.test.ts | 0 .../tests/public/semantic-contract.test.ts | 0 .../tests/public/signature-contract.test-d.ts | 0 .../tests/public/standard-conformance.test.ts | 0 ...sue-200-undo-overwrite-add-inverse.test.ts | 0 ...jsonpath-fastpath-object-container.test.ts | 0 .../issue-204-removerange-reducer.test.ts | 0 ...ema-introspection-optional-wrapper.test.ts | 0 ...e-208-readat-canonical-array-index.test.ts | 0 ...issue-210-paste-into-absent-target.test.ts | 0 .../issue-219-additive-surface.test.ts | 0 .../issue-219-editok-unification.test.ts | 0 .../issue-52-record-pointer.test-d.ts | 0 .../issue-55-applypatch-strict.test-d.ts | 0 .../issue-57-structural-sharing.test.ts | 0 .../regression/json-state-invariants.test.ts | 0 .../overlapping-replace-batch.test.ts | 0 .../prepared-change-reentrancy.test.ts | 0 .../structural-schema-fast-path.test.ts | 0 .../v1/labs}/extensions/README.md | 0 .../labs}/extensions/apply-defaults/README.md | 0 .../extensions/apply-defaults/package.json | 0 .../extensions/apply-defaults/src/create.ts | 0 .../extensions/apply-defaults/src/index.ts | 0 .../apply-defaults/src/operations.ts | 0 .../extensions/apply-defaults/src/plan.ts | 0 .../extensions/apply-defaults/src/types.ts | 0 .../tests/apply-defaults.test.ts | 0 .../extensions/apply-defaults/tsconfig.json | 0 .../apply-defaults/tsconfig.test.json | 0 .../apply-defaults/vitest.config.ts | 0 .../v1/labs}/extensions/autosave/README.md | 0 .../v1/labs}/extensions/autosave/package.json | 0 .../labs}/extensions/autosave/src/create.ts | 0 .../v1/labs}/extensions/autosave/src/index.ts | 0 .../v1/labs}/extensions/autosave/src/types.ts | 0 .../autosave/tests/autosave.test.ts | 0 .../labs}/extensions/autosave/tsconfig.json | 0 .../extensions/autosave/tsconfig.test.json | 0 .../extensions/autosave/vitest.config.ts | 0 .../labs}/extensions/batch-update/README.md | 0 .../extensions/batch-update/package.json | 0 .../extensions/batch-update/src/create.ts | 0 .../extensions/batch-update/src/index.ts | 0 .../extensions/batch-update/src/operations.ts | 0 .../labs}/extensions/batch-update/src/plan.ts | 0 .../extensions/batch-update/src/types.ts | 0 .../batch-update/tests/batch-update.test.ts | 0 .../extensions/batch-update/tsconfig.json | 0 .../batch-update/tsconfig.test.json | 0 .../extensions/batch-update/vitest.config.ts | 0 .../v1/labs}/extensions/bookmarks/README.md | 0 .../labs}/extensions/bookmarks/package.json | 0 .../labs}/extensions/bookmarks/src/create.ts | 0 .../labs}/extensions/bookmarks/src/index.ts | 0 .../labs}/extensions/bookmarks/src/types.ts | 0 .../bookmarks/tests/bookmarks.test.ts | 0 .../labs}/extensions/bookmarks/tsconfig.json | 0 .../extensions/bookmarks/tsconfig.test.json | 0 .../extensions/bookmarks/vitest.config.ts | 0 .../extensions/calculated-fields/README.md | 0 .../extensions/calculated-fields/package.json | 0 .../calculated-fields/src/create.ts | 0 .../extensions/calculated-fields/src/index.ts | 0 .../calculated-fields/src/operations.ts | 0 .../extensions/calculated-fields/src/plan.ts | 0 .../extensions/calculated-fields/src/types.ts | 0 .../tests/calculated-fields.test.ts | 0 .../calculated-fields/tsconfig.json | 0 .../calculated-fields/tsconfig.test.json | 0 .../calculated-fields/vitest.config.ts | 0 .../extensions/causal-patch-inbox/README.md | 0 .../causal-patch-inbox/package.json | 0 .../causal-patch-inbox/src/create.ts | 0 .../causal-patch-inbox/src/envelope.ts | 0 .../causal-patch-inbox/src/graph.ts | 0 .../causal-patch-inbox/src/index.ts | 0 .../causal-patch-inbox/src/ready.ts | 0 .../causal-patch-inbox/src/types.ts | 0 .../tests/causal-patch-inbox.test.ts | 0 .../causal-patch-inbox/tsconfig.json | 0 .../causal-patch-inbox/tsconfig.test.json | 0 .../causal-patch-inbox/vitest.config.ts | 0 .../v1/labs}/extensions/change-case/README.md | 0 .../labs}/extensions/change-case/package.json | 0 .../extensions/change-case/src/create.ts | 0 .../labs}/extensions/change-case/src/index.ts | 0 .../extensions/change-case/src/operations.ts | 0 .../labs}/extensions/change-case/src/plan.ts | 0 .../labs}/extensions/change-case/src/types.ts | 0 .../change-case/tests/change-case.test.ts | 0 .../extensions/change-case/tsconfig.json | 0 .../extensions/change-case/tsconfig.test.json | 0 .../extensions/change-case/vitest.config.ts | 0 .../v1/labs}/extensions/checkpoints/README.md | 0 .../labs}/extensions/checkpoints/package.json | 0 .../extensions/checkpoints/src/create.ts | 0 .../labs}/extensions/checkpoints/src/index.ts | 0 .../labs}/extensions/checkpoints/src/types.ts | 0 .../checkpoints/tests/checkpoints.test.ts | 0 .../extensions/checkpoints/tsconfig.json | 0 .../extensions/checkpoints/tsconfig.test.json | 0 .../extensions/checkpoints/vitest.config.ts | 0 .../labs}/extensions/clear-contents/README.md | 0 .../extensions/clear-contents/package.json | 0 .../extensions/clear-contents/src/create.ts | 0 .../extensions/clear-contents/src/index.ts | 0 .../clear-contents/src/operations.ts | 0 .../extensions/clear-contents/src/plan.ts | 0 .../extensions/clear-contents/src/types.ts | 0 .../tests/clear-contents.test.ts | 0 .../extensions/clear-contents/tsconfig.json | 0 .../clear-contents/tsconfig.test.json | 0 .../clear-contents/vitest.config.ts | 0 .../extensions/convert-block-type/README.md | 0 .../convert-block-type/package.json | 0 .../convert-block-type/src/create.ts | 0 .../convert-block-type/src/index.ts | 0 .../convert-block-type/src/operations.ts | 0 .../extensions/convert-block-type/src/plan.ts | 0 .../convert-block-type/src/types.ts | 0 .../tests/convert-block-type.test.ts | 0 .../convert-block-type/tsconfig.json | 0 .../convert-block-type/tsconfig.test.json | 0 .../convert-block-type/vitest.config.ts | 0 .../labs}/extensions/convert-type/README.md | 0 .../extensions/convert-type/package.json | 0 .../extensions/convert-type/src/create.ts | 0 .../extensions/convert-type/src/index.ts | 0 .../extensions/convert-type/src/operations.ts | 0 .../labs}/extensions/convert-type/src/plan.ts | 0 .../extensions/convert-type/src/types.ts | 0 .../convert-type/tests/convert-type.test.ts | 0 .../extensions/convert-type/tsconfig.json | 0 .../convert-type/tsconfig.test.json | 0 .../extensions/convert-type/vitest.config.ts | 0 .../v1/labs}/extensions/dedupe/README.md | 0 .../v1/labs}/extensions/dedupe/package.json | 0 .../v1/labs}/extensions/dedupe/src/create.ts | 0 .../v1/labs}/extensions/dedupe/src/index.ts | 0 .../labs}/extensions/dedupe/src/operations.ts | 0 .../v1/labs}/extensions/dedupe/src/plan.ts | 0 .../v1/labs}/extensions/dedupe/src/types.ts | 0 .../extensions/dedupe/tests/dedupe.test.ts | 0 .../v1/labs}/extensions/dedupe/tsconfig.json | 0 .../extensions/dedupe/tsconfig.test.json | 0 .../labs}/extensions/dedupe/vitest.config.ts | 0 .../labs}/extensions/document-diff/README.md | 0 .../extensions/document-diff/package.json | 0 .../extensions/document-diff/src/create.ts | 0 .../extensions/document-diff/src/index.ts | 0 .../document-diff/src/operations.ts | 0 .../extensions/document-diff/src/plan.ts | 0 .../extensions/document-diff/src/types.ts | 0 .../document-diff/tests/document-diff.test.ts | 0 .../extensions/document-diff/tsconfig.json | 0 .../document-diff/tsconfig.test.json | 0 .../extensions/document-diff/vitest.config.ts | 0 .../v1/labs}/extensions/drag-drop/README.md | 0 .../labs}/extensions/drag-drop/package.json | 0 .../labs}/extensions/drag-drop/src/create.ts | 0 .../labs}/extensions/drag-drop/src/index.ts | 0 .../extensions/drag-drop/src/operations.ts | 0 .../v1/labs}/extensions/drag-drop/src/plan.ts | 0 .../labs}/extensions/drag-drop/src/types.ts | 0 .../drag-drop/tests/drag-drop.test.ts | 0 .../labs}/extensions/drag-drop/tsconfig.json | 0 .../extensions/drag-drop/tsconfig.test.json | 0 .../extensions/drag-drop/vitest.config.ts | 0 .../v1/labs}/extensions/fill-blanks/README.md | 0 .../labs}/extensions/fill-blanks/package.json | 0 .../extensions/fill-blanks/src/create.ts | 0 .../labs}/extensions/fill-blanks/src/index.ts | 0 .../extensions/fill-blanks/src/operations.ts | 0 .../labs}/extensions/fill-blanks/src/plan.ts | 0 .../labs}/extensions/fill-blanks/src/types.ts | 0 .../fill-blanks/tests/fill-blanks.test.ts | 0 .../extensions/fill-blanks/tsconfig.json | 0 .../extensions/fill-blanks/tsconfig.test.json | 0 .../extensions/fill-blanks/vitest.config.ts | 0 .../v1/labs}/extensions/fill-down/README.md | 0 .../labs}/extensions/fill-down/package.json | 0 .../labs}/extensions/fill-down/src/create.ts | 0 .../labs}/extensions/fill-down/src/index.ts | 0 .../extensions/fill-down/src/operations.ts | 0 .../v1/labs}/extensions/fill-down/src/plan.ts | 0 .../labs}/extensions/fill-down/src/types.ts | 0 .../fill-down/tests/fill-down.test.ts | 0 .../labs}/extensions/fill-down/tsconfig.json | 0 .../extensions/fill-down/tsconfig.test.json | 0 .../extensions/fill-down/vitest.config.ts | 0 .../v1/labs}/extensions/fill-series/README.md | 0 .../labs}/extensions/fill-series/package.json | 0 .../extensions/fill-series/src/create.ts | 0 .../labs}/extensions/fill-series/src/index.ts | 0 .../extensions/fill-series/src/operations.ts | 0 .../labs}/extensions/fill-series/src/plan.ts | 0 .../labs}/extensions/fill-series/src/types.ts | 0 .../fill-series/tests/fill-series.test.ts | 0 .../extensions/fill-series/tsconfig.json | 0 .../extensions/fill-series/tsconfig.test.json | 0 .../extensions/fill-series/vitest.config.ts | 0 .../labs}/extensions/generate-slug/README.md | 0 .../extensions/generate-slug/package.json | 0 .../extensions/generate-slug/src/create.ts | 0 .../extensions/generate-slug/src/index.ts | 0 .../generate-slug/src/operations.ts | 0 .../extensions/generate-slug/src/plan.ts | 0 .../extensions/generate-slug/src/types.ts | 0 .../generate-slug/tests/generate-slug.test.ts | 0 .../extensions/generate-slug/tsconfig.json | 0 .../generate-slug/tsconfig.test.json | 0 .../extensions/generate-slug/vitest.config.ts | 0 .../v1/labs}/extensions/grid-range/README.md | 0 .../labs}/extensions/grid-range/package.json | 0 .../labs}/extensions/grid-range/src/create.ts | 0 .../labs}/extensions/grid-range/src/index.ts | 0 .../extensions/grid-range/src/operations.ts | 0 .../labs}/extensions/grid-range/src/plan.ts | 0 .../labs}/extensions/grid-range/src/types.ts | 0 .../grid-range/tests/grid-range.test.ts | 0 .../labs}/extensions/grid-range/tsconfig.json | 0 .../extensions/grid-range/tsconfig.test.json | 0 .../extensions/grid-range/vitest.config.ts | 0 .../extensions/increment-number/README.md | 0 .../extensions/increment-number/package.json | 0 .../extensions/increment-number/src/create.ts | 0 .../extensions/increment-number/src/index.ts | 0 .../increment-number/src/operations.ts | 0 .../extensions/increment-number/src/plan.ts | 0 .../extensions/increment-number/src/types.ts | 0 .../tests/increment-number.test.ts | 0 .../extensions/increment-number/tsconfig.json | 0 .../increment-number/tsconfig.test.json | 0 .../increment-number/vitest.config.ts | 0 .../v1/labs}/extensions/join-text/README.md | 0 .../labs}/extensions/join-text/package.json | 0 .../labs}/extensions/join-text/src/create.ts | 0 .../labs}/extensions/join-text/src/index.ts | 0 .../extensions/join-text/src/operations.ts | 0 .../v1/labs}/extensions/join-text/src/plan.ts | 0 .../labs}/extensions/join-text/src/types.ts | 0 .../join-text/tests/join-text.test.ts | 0 .../labs}/extensions/join-text/tsconfig.json | 0 .../extensions/join-text/tsconfig.test.json | 0 .../extensions/join-text/vitest.config.ts | 0 .../v1/labs}/extensions/layer-order/README.md | 0 .../labs}/extensions/layer-order/package.json | 0 .../extensions/layer-order/src/create.ts | 0 .../labs}/extensions/layer-order/src/index.ts | 0 .../extensions/layer-order/src/operations.ts | 0 .../labs}/extensions/layer-order/src/plan.ts | 0 .../labs}/extensions/layer-order/src/types.ts | 0 .../layer-order/tests/layer-order.test.ts | 0 .../extensions/layer-order/tsconfig.json | 0 .../extensions/layer-order/tsconfig.test.json | 0 .../extensions/layer-order/vitest.config.ts | 0 .../v1/labs}/extensions/limit-items/README.md | 0 .../labs}/extensions/limit-items/package.json | 0 .../extensions/limit-items/src/create.ts | 0 .../labs}/extensions/limit-items/src/index.ts | 0 .../extensions/limit-items/src/operations.ts | 0 .../labs}/extensions/limit-items/src/plan.ts | 0 .../labs}/extensions/limit-items/src/types.ts | 0 .../limit-items/tests/limit-items.test.ts | 0 .../extensions/limit-items/tsconfig.json | 0 .../extensions/limit-items/tsconfig.test.json | 0 .../extensions/limit-items/vitest.config.ts | 0 .../labs}/extensions/live-cursors/README.md | 0 .../extensions/live-cursors/package.json | 0 .../extensions/live-cursors/src/create.ts | 0 .../extensions/live-cursors/src/index.ts | 0 .../extensions/live-cursors/src/types.ts | 0 .../live-cursors/tests/live-cursors.test.ts | 0 .../extensions/live-cursors/tsconfig.json | 0 .../live-cursors/tsconfig.test.json | 0 .../extensions/live-cursors/vitest.config.ts | 0 .../labs}/extensions/move-selected/README.md | 0 .../extensions/move-selected/package.json | 0 .../extensions/move-selected/src/create.ts | 0 .../extensions/move-selected/src/index.ts | 0 .../move-selected/src/operations.ts | 0 .../extensions/move-selected/src/plan.ts | 0 .../extensions/move-selected/src/types.ts | 0 .../move-selected/tests/move-selected.test.ts | 0 .../extensions/move-selected/tsconfig.json | 0 .../move-selected/tsconfig.test.json | 0 .../extensions/move-selected/vitest.config.ts | 0 .../v1/labs}/extensions/pad-text/README.md | 0 .../v1/labs}/extensions/pad-text/package.json | 0 .../labs}/extensions/pad-text/src/create.ts | 0 .../v1/labs}/extensions/pad-text/src/index.ts | 0 .../extensions/pad-text/src/operations.ts | 0 .../v1/labs}/extensions/pad-text/src/plan.ts | 0 .../v1/labs}/extensions/pad-text/src/types.ts | 0 .../pad-text/tests/pad-text.test.ts | 0 .../labs}/extensions/pad-text/tsconfig.json | 0 .../extensions/pad-text/tsconfig.test.json | 0 .../extensions/pad-text/vitest.config.ts | 0 .../v1/labs}/extensions/paste-cells/README.md | 0 .../labs}/extensions/paste-cells/package.json | 0 .../extensions/paste-cells/src/create.ts | 0 .../labs}/extensions/paste-cells/src/index.ts | 0 .../extensions/paste-cells/src/operations.ts | 0 .../labs}/extensions/paste-cells/src/plan.ts | 0 .../labs}/extensions/paste-cells/src/types.ts | 0 .../paste-cells/tests/paste-cells.test.ts | 0 .../extensions/paste-cells/tsconfig.json | 0 .../extensions/paste-cells/tsconfig.test.json | 0 .../extensions/paste-cells/vitest.config.ts | 0 .../labs}/extensions/paste-special/README.md | 0 .../extensions/paste-special/package.json | 0 .../extensions/paste-special/src/create.ts | 0 .../extensions/paste-special/src/index.ts | 0 .../paste-special/src/operations.ts | 0 .../extensions/paste-special/src/plan.ts | 0 .../extensions/paste-special/src/types.ts | 0 .../paste-special/tests/paste-special.test.ts | 0 .../extensions/paste-special/tsconfig.json | 0 .../paste-special/tsconfig.test.json | 0 .../extensions/paste-special/vitest.config.ts | 0 .../labs}/extensions/patch-rebase/README.md | 0 .../extensions/patch-rebase/package.json | 0 .../labs}/extensions/patch-rebase/src/copy.ts | 0 .../extensions/patch-rebase/src/guards.ts | 0 .../extensions/patch-rebase/src/index.ts | 0 .../labs}/extensions/patch-rebase/src/plan.ts | 0 .../extensions/patch-rebase/src/pointer.ts | 0 .../extensions/patch-rebase/src/selection.ts | 0 .../extensions/patch-rebase/src/transform.ts | 0 .../extensions/patch-rebase/src/types.ts | 0 .../patch-rebase/tests/patch-rebase.test.ts | 0 .../extensions/patch-rebase/tsconfig.json | 0 .../patch-rebase/tsconfig.test.json | 0 .../extensions/patch-rebase/vitest.config.ts | 0 .../v1/labs}/extensions/references/README.md | 0 .../labs}/extensions/references/package.json | 0 .../labs}/extensions/references/src/create.ts | 0 .../labs}/extensions/references/src/index.ts | 0 .../extensions/references/src/operations.ts | 0 .../labs}/extensions/references/src/plan.ts | 0 .../labs}/extensions/references/src/types.ts | 0 .../references/tests/references.test.ts | 0 .../labs}/extensions/references/tsconfig.json | 0 .../extensions/references/tsconfig.test.json | 0 .../extensions/references/vitest.config.ts | 0 .../labs}/extensions/renumber-items/README.md | 0 .../extensions/renumber-items/package.json | 0 .../extensions/renumber-items/src/create.ts | 0 .../extensions/renumber-items/src/index.ts | 0 .../renumber-items/src/operations.ts | 0 .../extensions/renumber-items/src/plan.ts | 0 .../extensions/renumber-items/src/types.ts | 0 .../tests/renumber-items.test.ts | 0 .../extensions/renumber-items/tsconfig.json | 0 .../renumber-items/tsconfig.test.json | 0 .../renumber-items/vitest.config.ts | 0 .../v1/labs}/extensions/round/README.md | 0 .../v1/labs}/extensions/round/package.json | 0 .../v1/labs}/extensions/round/src/create.ts | 0 .../v1/labs}/extensions/round/src/index.ts | 0 .../labs}/extensions/round/src/operations.ts | 0 .../v1/labs}/extensions/round/src/plan.ts | 0 .../v1/labs}/extensions/round/src/types.ts | 0 .../extensions/round/tests/round.test.ts | 0 .../v1/labs}/extensions/round/tsconfig.json | 0 .../labs}/extensions/round/tsconfig.test.json | 0 .../labs}/extensions/round/vitest.config.ts | 0 .../v1/labs}/extensions/sort-items/README.md | 0 .../labs}/extensions/sort-items/package.json | 0 .../labs}/extensions/sort-items/src/create.ts | 0 .../labs}/extensions/sort-items/src/index.ts | 0 .../extensions/sort-items/src/operations.ts | 0 .../labs}/extensions/sort-items/src/plan.ts | 0 .../labs}/extensions/sort-items/src/types.ts | 0 .../sort-items/tests/sort-items.test.ts | 0 .../labs}/extensions/sort-items/tsconfig.json | 0 .../extensions/sort-items/tsconfig.test.json | 0 .../extensions/sort-items/vitest.config.ts | 0 .../labs}/extensions/sparse-record/README.md | 0 .../extensions/sparse-record/package.json | 0 .../extensions/sparse-record/src/create.ts | 0 .../extensions/sparse-record/src/index.ts | 0 .../sparse-record/src/operations.ts | 0 .../extensions/sparse-record/src/plan.ts | 0 .../extensions/sparse-record/src/types.ts | 0 .../sparse-record/tests/sparse-record.test.ts | 0 .../extensions/sparse-record/tsconfig.json | 0 .../sparse-record/tsconfig.test.json | 0 .../extensions/sparse-record/vitest.config.ts | 0 .../v1/labs}/extensions/split-text/README.md | 0 .../labs}/extensions/split-text/package.json | 0 .../labs}/extensions/split-text/src/create.ts | 0 .../labs}/extensions/split-text/src/index.ts | 0 .../extensions/split-text/src/operations.ts | 0 .../labs}/extensions/split-text/src/plan.ts | 0 .../labs}/extensions/split-text/src/types.ts | 0 .../split-text/tests/split-text.test.ts | 0 .../labs}/extensions/split-text/tsconfig.json | 0 .../extensions/split-text/tsconfig.test.json | 0 .../extensions/split-text/vitest.config.ts | 0 .../extensions/stable-id-rebase/README.md | 0 .../extensions/stable-id-rebase/package.json | 0 .../extensions/stable-id-rebase/src/index.ts | 0 .../extensions/stable-id-rebase/src/plan.ts | 0 .../stable-id-rebase/src/selection.ts | 0 .../extensions/stable-id-rebase/src/types.ts | 0 .../tests/stable-id-rebase.test.ts | 0 .../extensions/stable-id-rebase/tsconfig.json | 0 .../stable-id-rebase/tsconfig.test.json | 0 .../stable-id-rebase/vitest.config.ts | 0 .../v1/labs}/extensions/swap-items/README.md | 0 .../labs}/extensions/swap-items/package.json | 0 .../labs}/extensions/swap-items/src/create.ts | 0 .../labs}/extensions/swap-items/src/index.ts | 0 .../extensions/swap-items/src/operations.ts | 0 .../labs}/extensions/swap-items/src/plan.ts | 0 .../labs}/extensions/swap-items/src/types.ts | 0 .../swap-items/tests/swap-items.test.ts | 0 .../labs}/extensions/swap-items/tsconfig.json | 0 .../extensions/swap-items/tsconfig.test.json | 0 .../extensions/swap-items/vitest.config.ts | 0 .../labs}/extensions/toggle-option/README.md | 0 .../extensions/toggle-option/package.json | 0 .../extensions/toggle-option/src/create.ts | 0 .../extensions/toggle-option/src/index.ts | 0 .../extensions/toggle-option/src/plan.ts | 0 .../extensions/toggle-option/src/types.ts | 0 .../toggle-option/tests/toggle-option.test.ts | 0 .../extensions/toggle-option/tsconfig.json | 0 .../toggle-option/tsconfig.test.json | 0 .../extensions/toggle-option/vitest.config.ts | 0 .../labs}/extensions/toggle-value/README.md | 0 .../extensions/toggle-value/package.json | 0 .../extensions/toggle-value/src/create.ts | 0 .../extensions/toggle-value/src/index.ts | 0 .../extensions/toggle-value/src/operations.ts | 0 .../labs}/extensions/toggle-value/src/plan.ts | 0 .../extensions/toggle-value/src/types.ts | 0 .../toggle-value/tests/toggle-value.test.ts | 0 .../extensions/toggle-value/tsconfig.json | 0 .../toggle-value/tsconfig.test.json | 0 .../extensions/toggle-value/vitest.config.ts | 0 .../v1/labs}/extensions/trim-text/README.md | 0 .../labs}/extensions/trim-text/package.json | 0 .../labs}/extensions/trim-text/src/create.ts | 0 .../labs}/extensions/trim-text/src/index.ts | 0 .../extensions/trim-text/src/operations.ts | 0 .../v1/labs}/extensions/trim-text/src/plan.ts | 0 .../labs}/extensions/trim-text/src/types.ts | 0 .../trim-text/tests/trim-text.test.ts | 0 .../labs}/extensions/trim-text/tsconfig.json | 0 .../extensions/trim-text/tsconfig.test.json | 0 .../extensions/trim-text/vitest.config.ts | 0 .../labs}/extensions/wrap-selection/README.md | 0 .../extensions/wrap-selection/package.json | 0 .../extensions/wrap-selection/src/create.ts | 0 .../extensions/wrap-selection/src/index.ts | 0 .../wrap-selection/src/operations.ts | 0 .../extensions/wrap-selection/src/plan.ts | 0 .../extensions/wrap-selection/src/types.ts | 0 .../tests/wrap-selection.test.ts | 0 .../extensions/wrap-selection/tsconfig.json | 0 .../wrap-selection/tsconfig.test.json | 0 .../wrap-selection/vitest.config.ts | 0 .../v1/packages}/bulk-edit/README.md | 0 .../v1/packages}/bulk-edit/package.json | 0 .../v1/packages}/bulk-edit/src/create.ts | 0 .../v1/packages}/bulk-edit/src/deleteAll.ts | 0 .../v1/packages}/bulk-edit/src/errors.ts | 0 .../v1/packages}/bulk-edit/src/index.ts | 0 .../v1/packages}/bulk-edit/src/matches.ts | 0 .../v1/packages}/bulk-edit/src/replaceAll.ts | 0 .../v1/packages}/bulk-edit/src/types.ts | 0 .../bulk-edit/tests/bulk-edit.test.ts | 0 .../v1/packages}/bulk-edit/tsconfig.json | 0 .../v1/packages}/bulk-edit/tsconfig.test.json | 0 .../v1/packages}/bulk-edit/vitest.config.ts | 0 .../v1/packages}/clipboard-web/README.md | 0 .../v1/packages}/clipboard-web/package.json | 0 .../v1/packages}/clipboard-web/src/codec.ts | 0 .../packages}/clipboard-web/src/constants.ts | 0 .../v1/packages}/clipboard-web/src/create.ts | 0 .../v1/packages}/clipboard-web/src/errors.ts | 0 .../v1/packages}/clipboard-web/src/host.ts | 0 .../v1/packages}/clipboard-web/src/index.ts | 0 .../v1/packages}/clipboard-web/src/paste.ts | 0 .../v1/packages}/clipboard-web/src/types.ts | 0 .../clipboard-web/tests/clipboard-web.test.ts | 0 .../v1/packages}/clipboard-web/tsconfig.json | 0 .../clipboard-web/tsconfig.test.json | 0 .../packages}/clipboard-web/vitest.config.ts | 0 .../v1/packages}/collection/README.md | 0 .../v1/packages}/collection/package.json | 0 .../v1/packages}/collection/src/create.ts | 0 .../v1/packages}/collection/src/delete.ts | 0 .../v1/packages}/collection/src/duplicate.ts | 0 .../v1/packages}/collection/src/error.ts | 0 .../v1/packages}/collection/src/index.ts | 0 .../v1/packages}/collection/src/location.ts | 0 .../v1/packages}/collection/src/move.ts | 0 .../v1/packages}/collection/src/types.ts | 0 .../collection/tests/collection.test.ts | 0 .../v1/packages}/collection/tsconfig.json | 0 .../packages}/collection/tsconfig.test.json | 0 .../v1/packages}/collection/vitest.config.ts | 0 .../v1/packages}/comments/README.md | 0 .../v1/packages}/comments/package.json | 0 .../v1/packages}/comments/src/copy.ts | 0 .../v1/packages}/comments/src/create.ts | 0 .../v1/packages}/comments/src/errors.ts | 0 .../v1/packages}/comments/src/filter.ts | 0 .../v1/packages}/comments/src/index.ts | 0 .../v1/packages}/comments/src/snapshot.ts | 0 .../v1/packages}/comments/src/status.ts | 0 .../v1/packages}/comments/src/tracking.ts | 0 .../v1/packages}/comments/src/types.ts | 0 .../v1/packages}/comments/src/validation.ts | 0 .../packages}/comments/tests/comments.test.ts | 0 .../v1/packages}/comments/tsconfig.json | 0 .../v1/packages}/comments/tsconfig.test.json | 0 .../v1/packages}/comments/vitest.config.ts | 0 .../packages}/contenteditable-react/README.md | 0 .../contenteditable-react/package.json | 0 .../contenteditable-react/src/index.ts | 0 .../src/useContentEditable.ts | 0 .../tests/contenteditable-react.test.tsx | 0 .../contenteditable-react/tsconfig.json | 0 .../contenteditable-react/tsconfig.test.json | 0 .../contenteditable-react/vitest.config.ts | 0 .../packages}/contenteditable-web/README.md | 0 .../contenteditable-web/package.json | 0 .../contenteditable-web/src/constants.ts | 0 .../packages}/contenteditable-web/src/core.ts | 0 .../src/dom/clipboardEvent.ts | 0 .../src/dom/createAdapter.ts | 0 .../contenteditable-web/src/dom/selection.ts | 0 .../src/dom/textProjection.ts | 0 .../contenteditable-web/src/fragment.ts | 0 .../contenteditable-web/src/index.ts | 0 .../contenteditable-web/src/types.ts | 0 .../tests/contenteditable-core.test.ts | 0 .../tests/contenteditable-web.test.ts | 0 .../contenteditable-web/tsconfig.json | 0 .../contenteditable-web/tsconfig.test.json | 0 .../contenteditable-web/vitest.config.ts | 0 .../v1/packages}/dirty-state/README.md | 0 .../v1/packages}/dirty-state/package.json | 0 .../v1/packages}/dirty-state/src/create.ts | 0 .../v1/packages}/dirty-state/src/index.ts | 0 .../v1/packages}/dirty-state/src/snapshot.ts | 0 .../v1/packages}/dirty-state/src/types.ts | 0 .../dirty-state/tests/dirty-state.test.ts | 0 .../v1/packages}/dirty-state/tsconfig.json | 0 .../packages}/dirty-state/tsconfig.test.json | 0 .../v1/packages}/dirty-state/vitest.config.ts | 0 .../v1/packages}/form-draft/README.md | 0 .../v1/packages}/form-draft/package.json | 0 .../v1/packages}/form-draft/src/batch.ts | 0 .../v1/packages}/form-draft/src/create.ts | 0 .../v1/packages}/form-draft/src/errors.ts | 0 .../v1/packages}/form-draft/src/events.ts | 0 .../v1/packages}/form-draft/src/index.ts | 0 .../v1/packages}/form-draft/src/json.ts | 0 .../v1/packages}/form-draft/src/snapshot.ts | 0 .../v1/packages}/form-draft/src/types.ts | 0 .../form-draft/tests/form-draft.test.ts | 0 .../v1/packages}/form-draft/tsconfig.json | 0 .../packages}/form-draft/tsconfig.test.json | 0 .../v1/packages}/form-draft/vitest.config.ts | 0 .../v1/packages}/grouping/LICENSE | 0 .../v1/packages}/grouping/README.md | 0 .../v1/packages}/grouping/package.json | 0 .../v1/packages}/grouping/src/create.ts | 0 .../v1/packages}/grouping/src/index.ts | 0 .../v1/packages}/grouping/src/operations.ts | 0 .../v1/packages}/grouping/src/plan.ts | 0 .../v1/packages}/grouping/src/types.ts | 0 .../packages}/grouping/tests/grouping.test.ts | 0 .../v1/packages}/grouping/tsconfig.json | 0 .../v1/packages}/grouping/tsconfig.test.json | 0 .../v1/packages}/grouping/vitest.config.ts | 0 .../v1/packages}/id-resolver/README.md | 0 .../v1/packages}/id-resolver/package.json | 0 .../v1/packages}/id-resolver/src/create.ts | 0 .../packages}/id-resolver/src/diagnostics.ts | 0 .../v1/packages}/id-resolver/src/index.ts | 0 .../v1/packages}/id-resolver/src/resolve.ts | 0 .../v1/packages}/id-resolver/src/snapshot.ts | 0 .../v1/packages}/id-resolver/src/types.ts | 0 .../id-resolver/tests/id-resolver.test.ts | 0 .../v1/packages}/id-resolver/tsconfig.json | 0 .../packages}/id-resolver/tsconfig.test.json | 0 .../v1/packages}/id-resolver/vitest.config.ts | 0 .../v1/packages}/outline/README.md | 0 .../v1/packages}/outline/package.json | 0 .../v1/packages}/outline/src/copy.ts | 0 .../v1/packages}/outline/src/create.ts | 0 .../v1/packages}/outline/src/error.ts | 0 .../v1/packages}/outline/src/index.ts | 0 .../v1/packages}/outline/src/move.ts | 0 .../v1/packages}/outline/src/options.ts | 0 .../v1/packages}/outline/src/read.ts | 0 .../v1/packages}/outline/src/source.ts | 0 .../v1/packages}/outline/src/types.ts | 0 .../packages}/outline/tests/outline.test.ts | 0 .../v1/packages}/outline/tsconfig.json | 0 .../v1/packages}/outline/tsconfig.test.json | 0 .../v1/packages}/outline/vitest.config.ts | 0 .../v1/packages}/patch-log/README.md | 0 .../v1/packages}/patch-log/package.json | 0 .../v1/packages}/patch-log/src/copy.ts | 0 .../v1/packages}/patch-log/src/create.ts | 0 .../v1/packages}/patch-log/src/index.ts | 0 .../v1/packages}/patch-log/src/replay.ts | 0 .../v1/packages}/patch-log/src/store.ts | 0 .../v1/packages}/patch-log/src/types.ts | 0 .../patch-log/tests/patch-log.test.ts | 0 .../v1/packages}/patch-log/tsconfig.json | 0 .../v1/packages}/patch-log/tsconfig.test.json | 0 .../v1/packages}/patch-log/vitest.config.ts | 0 .../v1/packages}/patch-preview/LICENSE | 0 .../v1/packages}/patch-preview/README.md | 0 .../v1/packages}/patch-preview/package.json | 0 .../v1/packages}/patch-preview/src/create.ts | 0 .../v1/packages}/patch-preview/src/index.ts | 0 .../v1/packages}/patch-preview/src/preview.ts | 0 .../v1/packages}/patch-preview/src/types.ts | 0 .../patch-preview/tests/patch-preview.test.ts | 0 .../v1/packages}/patch-preview/tsconfig.json | 0 .../patch-preview/tsconfig.test.json | 0 .../packages}/patch-preview/vitest.config.ts | 0 .../v1/packages}/persist-web/README.md | 0 .../v1/packages}/persist-web/package.json | 0 .../v1/packages}/persist-web/src/codec.ts | 0 .../v1/packages}/persist-web/src/constants.ts | 0 .../v1/packages}/persist-web/src/create.ts | 0 .../v1/packages}/persist-web/src/errors.ts | 0 .../v1/packages}/persist-web/src/host.ts | 0 .../v1/packages}/persist-web/src/index.ts | 0 .../v1/packages}/persist-web/src/restore.ts | 0 .../v1/packages}/persist-web/src/selection.ts | 0 .../v1/packages}/persist-web/src/snapshot.ts | 0 .../v1/packages}/persist-web/src/types.ts | 0 .../v1/packages}/persist-web/src/watch.ts | 0 .../persist-web/tests/persist-web.test.ts | 0 .../v1/packages}/persist-web/tsconfig.json | 0 .../packages}/persist-web/tsconfig.test.json | 0 .../v1/packages}/persist-web/vitest.config.ts | 0 .../v1/packages}/proposed-changes/README.md | 0 .../packages}/proposed-changes/package.json | 0 .../packages}/proposed-changes/src/accept.ts | 0 .../v1/packages}/proposed-changes/src/copy.ts | 0 .../packages}/proposed-changes/src/create.ts | 0 .../packages}/proposed-changes/src/errors.ts | 0 .../packages}/proposed-changes/src/index.ts | 0 .../v1/packages}/proposed-changes/src/plan.ts | 0 .../packages}/proposed-changes/src/store.ts | 0 .../packages}/proposed-changes/src/types.ts | 0 .../tests/proposed-changes.test.ts | 0 .../packages}/proposed-changes/tsconfig.json | 0 .../proposed-changes/tsconfig.test.json | 0 .../proposed-changes/vitest.config.ts | 0 .../v1/packages}/protected-ranges/README.md | 0 .../packages}/protected-ranges/package.json | 0 .../v1/packages}/protected-ranges/src/copy.ts | 0 .../packages}/protected-ranges/src/create.ts | 0 .../packages}/protected-ranges/src/index.ts | 0 .../packages}/protected-ranges/src/match.ts | 0 .../protected-ranges/src/operations.ts | 0 .../packages}/protected-ranges/src/types.ts | 0 .../tests/protected-ranges.test.ts | 0 .../packages}/protected-ranges/tsconfig.json | 0 .../protected-ranges/tsconfig.test.json | 0 .../protected-ranges/vitest.config.ts | 0 .../v1/packages}/schema-form/README.md | 0 .../v1/packages}/schema-form/package.json | 0 .../v1/packages}/schema-form/src/container.ts | 0 .../v1/packages}/schema-form/src/create.ts | 0 .../v1/packages}/schema-form/src/error.ts | 0 .../v1/packages}/schema-form/src/field.ts | 0 .../v1/packages}/schema-form/src/index.ts | 0 .../v1/packages}/schema-form/src/tree.ts | 0 .../v1/packages}/schema-form/src/types.ts | 0 .../schema-form/tests/schema-form.test.ts | 0 .../v1/packages}/schema-form/tsconfig.json | 0 .../packages}/schema-form/tsconfig.test.json | 0 .../v1/packages}/schema-form/vitest.config.ts | 0 .../v1/packages}/search-replace/LICENSE | 0 .../v1/packages}/search-replace/README.md | 0 .../v1/packages}/search-replace/package.json | 0 .../v1/packages}/search-replace/src/create.ts | 0 .../v1/packages}/search-replace/src/errors.ts | 0 .../v1/packages}/search-replace/src/find.ts | 0 .../v1/packages}/search-replace/src/index.ts | 0 .../v1/packages}/search-replace/src/match.ts | 0 .../search-replace/src/replaceAll.ts | 0 .../v1/packages}/search-replace/src/types.ts | 0 .../tests/search-replace.test.ts | 0 .../v1/packages}/search-replace/tsconfig.json | 0 .../search-replace/tsconfig.test.json | 0 .../packages}/search-replace/vitest.config.ts | 0 .../v1/packages}/snippets/README.md | 0 .../v1/packages}/snippets/package.json | 0 .../v1/packages}/snippets/src/copy.ts | 0 .../v1/packages}/snippets/src/create.ts | 0 .../v1/packages}/snippets/src/errors.ts | 0 .../v1/packages}/snippets/src/index.ts | 0 .../v1/packages}/snippets/src/insert.ts | 0 .../v1/packages}/snippets/src/options.ts | 0 .../v1/packages}/snippets/src/plan.ts | 0 .../v1/packages}/snippets/src/store.ts | 0 .../v1/packages}/snippets/src/types.ts | 0 .../packages}/snippets/tests/snippets.test.ts | 0 .../v1/packages}/snippets/tsconfig.json | 0 .../v1/packages}/snippets/tsconfig.test.json | 0 .../v1/packages}/snippets/vitest.config.ts | 0 .../scripts}/benchmark-adapter-readiness.mjs | 0 .../v1/scripts}/benchmark-causal.mjs | 0 .../v1/scripts}/evaluate-extension-lab.mjs | 0 .../v1/scripts}/evaluate-extensions.mjs | 0 .../v1/scripts}/run-workspace-scripts.mjs | 0 config/json-document-source-aliases.ts | 59 +- docs/README.md | 38 +- docs/changelog.md | 317 +- docs/generated/extensions-catalog.md | 77 - docs/generated/repo-catalog.json | 3203 +-------------- docs/public/overview.md | 17 +- llms.txt | 39 +- package-lock.json | 3641 +++-------------- package.json | 29 +- packages/json-document/package.json | 7 +- packages/json-document/tsconfig.test.json | 6 - packages/json-document/vitest.config.ts | 2 - scripts/ci-scope.mjs | 38 +- scripts/evaluate-archive-isolation.mjs | 84 + scripts/evaluate-docs.mjs | 512 +-- scripts/generate-docs.mjs | 410 +- tests/browser/outliner.spec.ts | 91 - tests/browser/site.spec.ts | 23 +- tsconfig.json-document-paths.json | 3 - 989 files changed, 1342 insertions(+), 13578 deletions(-) delete mode 100644 apps/site/src/generated/repo-catalog.ts delete mode 100644 apps/site/src/playgrounds/InterfaceWorkbench.playground.tsx delete mode 100644 apps/site/src/routes/Playground.tsx delete mode 100644 apps/site/tests/interface-workbench.test.tsx create mode 100644 archive/v1/README.md rename {apps => archive/v1/apps}/copy-review-lab/README.md (100%) rename {apps => archive/v1/apps}/copy-review-lab/index.html (100%) rename {apps => archive/v1/apps}/copy-review-lab/package.json (100%) rename {apps => archive/v1/apps}/copy-review-lab/src/App.tsx (100%) rename {apps => archive/v1/apps}/copy-review-lab/src/copy-review-lab.css (100%) rename {apps => archive/v1/apps}/copy-review-lab/src/main.tsx (100%) rename {apps => archive/v1/apps}/copy-review-lab/tests/copy-review-lab.test.tsx (100%) rename {apps => archive/v1/apps}/copy-review-lab/tsconfig.json (100%) rename {apps => archive/v1/apps}/copy-review-lab/vite.config.ts (100%) rename {apps => archive/v1/apps}/grouping-lab/README.md (100%) rename {apps => archive/v1/apps}/grouping-lab/index.html (100%) rename {apps => archive/v1/apps}/grouping-lab/package.json (100%) rename {apps => archive/v1/apps}/grouping-lab/src/App.tsx (100%) rename {apps => archive/v1/apps}/grouping-lab/src/grouping-lab.css (100%) rename {apps => archive/v1/apps}/grouping-lab/src/main.tsx (100%) rename {apps => archive/v1/apps}/grouping-lab/tests/grouping-lab.test.tsx (100%) rename {apps => archive/v1/apps}/grouping-lab/tsconfig.json (100%) rename {apps => archive/v1/apps}/grouping-lab/vite.config.ts (100%) rename {apps => archive/v1/apps}/mobile-cms/package.json (100%) rename {apps => archive/v1/apps}/mobile-cms/src/App.tsx (100%) rename {apps => archive/v1/apps}/mobile-cms/src/mobile-cms.css (100%) rename {apps => archive/v1/apps}/mobile-cms/tests/mobile-cms-ux.test.tsx (100%) rename {apps => archive/v1/apps}/mobile-cms/tsconfig.json (100%) rename {apps => archive/v1/apps}/mobile-cms/vitest.config.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-a/index.html (100%) rename {apps => archive/v1/apps}/outliner-blind-a/package.json (100%) rename {apps => archive/v1/apps}/outliner-blind-a/src/App.tsx (100%) rename {apps => archive/v1/apps}/outliner-blind-a/src/main.tsx (100%) rename {apps => archive/v1/apps}/outliner-blind-a/src/outline.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-a/src/schema.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-a/src/styles.css (100%) rename {apps => archive/v1/apps}/outliner-blind-a/tests/outliner-blind-a.test.tsx (100%) rename {apps => archive/v1/apps}/outliner-blind-a/tsconfig.json (100%) rename {apps => archive/v1/apps}/outliner-blind-a/vite.config.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-a/vitest.config.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-b/index.html (100%) rename {apps => archive/v1/apps}/outliner-blind-b/package.json (100%) rename {apps => archive/v1/apps}/outliner-blind-b/src/App.tsx (100%) rename {apps => archive/v1/apps}/outliner-blind-b/src/main.tsx (100%) rename {apps => archive/v1/apps}/outliner-blind-b/src/outlinerModel.test.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-b/src/outlinerModel.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-b/src/styles.css (100%) rename {apps => archive/v1/apps}/outliner-blind-b/tsconfig.json (100%) rename {apps => archive/v1/apps}/outliner-blind-b/vite.config.ts (100%) rename {apps => archive/v1/apps}/outliner-blind-b/vitest.config.ts (100%) rename {apps => archive/v1/apps}/outliner/package.json (100%) rename {apps => archive/v1/apps}/outliner/src/OutlineRow.tsx (100%) rename {apps => archive/v1/apps}/outliner/src/Outliner.tsx (100%) rename {apps => archive/v1/apps}/outliner/src/clipboard.ts (100%) rename {apps => archive/v1/apps}/outliner/src/commands/clipboard.ts (100%) rename {apps => archive/v1/apps}/outliner/src/commands/context.ts (100%) rename {apps => archive/v1/apps}/outliner/src/commands/focus.ts (100%) rename {apps => archive/v1/apps}/outliner/src/commands/index.ts (100%) rename {apps => archive/v1/apps}/outliner/src/commands/selection.ts (100%) rename {apps => archive/v1/apps}/outliner/src/commands/structure.ts (100%) rename {apps => archive/v1/apps}/outliner/src/hooks/useClickPolicy.ts (100%) rename {apps => archive/v1/apps}/outliner/src/hooks/useDispatch.ts (100%) rename {apps => archive/v1/apps}/outliner/src/hooks/useDraftState.ts (100%) rename {apps => archive/v1/apps}/outliner/src/hooks/useGlobalKey.ts (100%) rename {apps => archive/v1/apps}/outliner/src/hooks/useTextEditCoalesce.ts (100%) rename {apps => archive/v1/apps}/outliner/src/hooks/useToasts.ts (100%) rename {apps => archive/v1/apps}/outliner/src/index.ts (100%) rename {apps => archive/v1/apps}/outliner/src/keymap.ts (100%) rename {apps => archive/v1/apps}/outliner/src/outliner.css (100%) rename {apps => archive/v1/apps}/outliner/src/pointer-utils.ts (100%) rename {apps => archive/v1/apps}/outliner/src/schema.ts (100%) rename {apps => archive/v1/apps}/outliner/tests/coverage-keyboard-mouse.test.tsx (100%) rename {apps => archive/v1/apps}/outliner/tests/outliner-history.test.tsx (100%) rename {apps => archive/v1/apps}/outliner/tests/outliner-interactions.test.tsx (100%) rename {apps => archive/v1/apps}/outliner/tests/selection-auto-rules.test.tsx (100%) rename {apps => archive/v1/apps}/outliner/tests/stress.test.tsx (100%) rename {apps => archive/v1/apps}/outliner/tsconfig.json (100%) rename {apps => archive/v1/apps}/outliner/vitest.config.ts (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/README.md (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/index.html (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/package.json (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/src/App.tsx (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/src/main.tsx (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/src/proposed-changes-lab.css (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/tests/proposed-changes-lab.test.tsx (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/tsconfig.json (100%) rename {apps => archive/v1/apps}/proposed-changes-lab/vite.config.ts (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/README.md (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/index.html (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/package.json (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/src/App.tsx (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/src/main.tsx (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/src/protected-ranges-lab.css (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/tests/protected-ranges-lab.test.tsx (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/tsconfig.json (100%) rename {apps => archive/v1/apps}/protected-ranges-lab/vite.config.ts (100%) rename {apps => archive/v1/apps}/review-comments-lab/README.md (100%) rename {apps => archive/v1/apps}/review-comments-lab/index.html (100%) rename {apps => archive/v1/apps}/review-comments-lab/package.json (100%) rename {apps => archive/v1/apps}/review-comments-lab/src/App.tsx (100%) rename {apps => archive/v1/apps}/review-comments-lab/src/main.tsx (100%) rename {apps => archive/v1/apps}/review-comments-lab/src/review-comments-lab.css (100%) rename {apps => archive/v1/apps}/review-comments-lab/tests/review-comments-lab.test.tsx (100%) rename {apps => archive/v1/apps}/review-comments-lab/tsconfig.json (100%) rename {apps => archive/v1/apps}/review-comments-lab/vite.config.ts (100%) rename {apps => archive/v1/apps}/schema-form-lab/index.html (100%) rename {apps => archive/v1/apps}/schema-form-lab/package.json (100%) rename {apps => archive/v1/apps}/schema-form-lab/src/App.tsx (100%) rename {apps => archive/v1/apps}/schema-form-lab/src/main.tsx (100%) rename {apps => archive/v1/apps}/schema-form-lab/src/schema-form-lab.css (100%) rename {apps => archive/v1/apps}/schema-form-lab/tests/schema-form-lab.test.tsx (100%) rename {apps => archive/v1/apps}/schema-form-lab/tsconfig.json (100%) rename {apps => archive/v1/apps}/schema-form-lab/vite.config.ts (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/README.md (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/index.html (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/package.json (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/src/App.tsx (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/src/main.tsx (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/src/snippet-composer-lab.css (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/tests/snippet-composer-lab.test.tsx (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/tsconfig.json (100%) rename {apps => archive/v1/apps}/snippet-composer-lab/vite.config.ts (100%) create mode 100644 archive/v1/docs/changelog.md rename {docs => archive/v1/docs}/public/extensions.md (100%) rename {docs => archive/v1/docs}/public/recipes.md (100%) rename {docs => archive/v1/docs}/research/de-facto-editing-feature-taxonomy.md (100%) rename {docs => archive/v1/docs}/standard/conformance-profile.md (100%) rename {docs => archive/v1/docs}/standard/contract-pressure-register.md (100%) rename {docs => archive/v1/docs}/standard/core-standard.md (100%) rename {docs => archive/v1/docs}/standard/extension-delegation-standard.md (100%) rename {docs => archive/v1/docs}/standard/foundation-gate.md (100%) rename {docs => archive/v1/docs}/standard/json-document-spec.md (100%) rename {docs => archive/v1/docs}/standard/public-api-layering.md (100%) rename {docs => archive/v1/docs}/standard/result-contract.md (100%) rename {docs => archive/v1/docs}/standard/schema-introspection-contract.md (100%) rename {docs => archive/v1/docs}/standard/selection-contract.md (99%) rename {docs => archive/v1/docs}/standard/self-improvement-loop-report.md (100%) rename {packages => archive/v1}/json-document/src/application/react-document/index.ts (100%) rename {packages => archive/v1}/json-document/src/application/session/contract.ts (100%) rename {packages => archive/v1}/json-document/src/application/session/create.ts (100%) rename {packages => archive/v1}/json-document/src/application/session/index.ts (100%) rename {packages => archive/v1}/json-document/src/domain/clipboard/copy.ts (100%) rename {packages => archive/v1}/json-document/src/domain/clipboard/cut.ts (100%) rename {packages => archive/v1}/json-document/src/domain/clipboard/paste.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/capabilities/create.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/capabilities/result.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/clipboard/buffer.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/clipboard/clipboard.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/clipboard/contract.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/clipboard/paste.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/create.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/editing/actions.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/editing/plan.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/editing/target.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/history/metadata.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/history/restore.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/history/undoRedo.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/index.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/reading/read.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/schema/accepts.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/schema/description.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/schema/query.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/schema/resolve.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/schema/state.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/selection/create.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/selection/runtime.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/state/json.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/state/ops.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/state/ownership.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/state/patch.ts (100%) rename {packages => archive/v1}/json-document/src/domain/document/text-surface/index.ts (100%) rename {packages => archive/v1}/json-document/src/domain/editing/duplicate.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/array/add.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/array/path.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/array/replace.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/inspection/introspection.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/inspection/zod.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/model/knownJson.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/model/result.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/model/schema.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/model/value.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/mutation/patch.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/mutation/rekey.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/object/record.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/object/replace.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/object/value.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/validation/patch.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/validation/replace.ts (100%) rename {packages => archive/v1}/json-document/src/domain/schema/validation/schema.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/autoRules.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/order.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/point.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/read.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/reducer.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/snap.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/spans.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/textDelete.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/textEdit.ts (100%) rename {packages => archive/v1}/json-document/src/domain/selection/traversal.ts (100%) rename {packages => archive/v1}/json-document/src/domain/text-surface/surface.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/error/index.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/history/index.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/patch/index.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/patch/inverse.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/patch/schema-contract.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/patch/schema.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/patch/source.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/pointer/index.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/pointer/siblingRange.ts (100%) rename {packages => archive/v1}/json-document/src/foundation/pointer/source.ts (100%) rename {packages => archive/v1}/json-document/tests/document/capability/integration.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/clipboard/integration.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/clipboard/react.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/create-json-document.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/history/load-react.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/history/metadata.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/interface.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/read/integration.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/read/react.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/schema/integration.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/schema/react.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/selection/headless.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/selection/react.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/text-surface.test.ts (100%) rename {packages => archive/v1}/json-document/tests/document/validation-paths.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/error.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/guarantees.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/json-patch/apply.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/json-patch/conformance.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/json-patch/rfc6902.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/json-pointer/sibling-range.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/json-pointer/uri-fragment.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/jsonpath/cts.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/pointer-tracking/pointer.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/result-contract.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/semantic-contract.test.ts (100%) rename {packages => archive/v1}/json-document/tests/public/signature-contract.test-d.ts (100%) rename {packages => archive/v1}/json-document/tests/public/standard-conformance.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-200-undo-overwrite-add-inverse.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-202-jsonpath-fastpath-object-container.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-204-removerange-reducer.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-206-schema-introspection-optional-wrapper.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-208-readat-canonical-array-index.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-210-paste-into-absent-target.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-219-additive-surface.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-219-editok-unification.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-52-record-pointer.test-d.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-55-applypatch-strict.test-d.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/issue-57-structural-sharing.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/json-state-invariants.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/overlapping-replace-batch.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/prepared-change-reentrancy.test.ts (100%) rename {packages => archive/v1}/json-document/tests/regression/structural-schema-fast-path.test.ts (100%) rename {labs => archive/v1/labs}/extensions/README.md (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/README.md (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/package.json (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/tests/apply-defaults.test.ts (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/apply-defaults/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/autosave/README.md (100%) rename {labs => archive/v1/labs}/extensions/autosave/package.json (100%) rename {labs => archive/v1/labs}/extensions/autosave/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/autosave/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/autosave/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/autosave/tests/autosave.test.ts (100%) rename {labs => archive/v1/labs}/extensions/autosave/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/autosave/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/autosave/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/batch-update/README.md (100%) rename {labs => archive/v1/labs}/extensions/batch-update/package.json (100%) rename {labs => archive/v1/labs}/extensions/batch-update/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/batch-update/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/batch-update/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/batch-update/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/batch-update/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/batch-update/tests/batch-update.test.ts (100%) rename {labs => archive/v1/labs}/extensions/batch-update/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/batch-update/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/batch-update/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/README.md (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/package.json (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/tests/bookmarks.test.ts (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/bookmarks/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/README.md (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/package.json (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/tests/calculated-fields.test.ts (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/calculated-fields/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/README.md (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/package.json (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/src/envelope.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/src/graph.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/src/ready.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/tests/causal-patch-inbox.test.ts (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/causal-patch-inbox/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/change-case/README.md (100%) rename {labs => archive/v1/labs}/extensions/change-case/package.json (100%) rename {labs => archive/v1/labs}/extensions/change-case/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/change-case/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/change-case/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/change-case/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/change-case/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/change-case/tests/change-case.test.ts (100%) rename {labs => archive/v1/labs}/extensions/change-case/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/change-case/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/change-case/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/README.md (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/package.json (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/tests/checkpoints.test.ts (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/checkpoints/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/README.md (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/package.json (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/tests/clear-contents.test.ts (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/clear-contents/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/README.md (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/package.json (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/tests/convert-block-type.test.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/convert-block-type/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-type/README.md (100%) rename {labs => archive/v1/labs}/extensions/convert-type/package.json (100%) rename {labs => archive/v1/labs}/extensions/convert-type/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-type/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-type/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-type/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-type/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-type/tests/convert-type.test.ts (100%) rename {labs => archive/v1/labs}/extensions/convert-type/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/convert-type/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/convert-type/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/dedupe/README.md (100%) rename {labs => archive/v1/labs}/extensions/dedupe/package.json (100%) rename {labs => archive/v1/labs}/extensions/dedupe/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/dedupe/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/dedupe/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/dedupe/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/dedupe/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/dedupe/tests/dedupe.test.ts (100%) rename {labs => archive/v1/labs}/extensions/dedupe/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/dedupe/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/dedupe/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/document-diff/README.md (100%) rename {labs => archive/v1/labs}/extensions/document-diff/package.json (100%) rename {labs => archive/v1/labs}/extensions/document-diff/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/document-diff/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/document-diff/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/document-diff/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/document-diff/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/document-diff/tests/document-diff.test.ts (100%) rename {labs => archive/v1/labs}/extensions/document-diff/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/document-diff/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/document-diff/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/README.md (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/package.json (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/tests/drag-drop.test.ts (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/drag-drop/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/README.md (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/package.json (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/tests/fill-blanks.test.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/fill-blanks/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-down/README.md (100%) rename {labs => archive/v1/labs}/extensions/fill-down/package.json (100%) rename {labs => archive/v1/labs}/extensions/fill-down/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-down/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-down/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-down/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-down/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-down/tests/fill-down.test.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-down/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/fill-down/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/fill-down/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-series/README.md (100%) rename {labs => archive/v1/labs}/extensions/fill-series/package.json (100%) rename {labs => archive/v1/labs}/extensions/fill-series/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-series/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-series/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-series/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-series/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-series/tests/fill-series.test.ts (100%) rename {labs => archive/v1/labs}/extensions/fill-series/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/fill-series/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/fill-series/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/README.md (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/package.json (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/tests/generate-slug.test.ts (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/generate-slug/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/grid-range/README.md (100%) rename {labs => archive/v1/labs}/extensions/grid-range/package.json (100%) rename {labs => archive/v1/labs}/extensions/grid-range/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/grid-range/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/grid-range/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/grid-range/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/grid-range/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/grid-range/tests/grid-range.test.ts (100%) rename {labs => archive/v1/labs}/extensions/grid-range/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/grid-range/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/grid-range/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/increment-number/README.md (100%) rename {labs => archive/v1/labs}/extensions/increment-number/package.json (100%) rename {labs => archive/v1/labs}/extensions/increment-number/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/increment-number/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/increment-number/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/increment-number/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/increment-number/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/increment-number/tests/increment-number.test.ts (100%) rename {labs => archive/v1/labs}/extensions/increment-number/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/increment-number/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/increment-number/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/join-text/README.md (100%) rename {labs => archive/v1/labs}/extensions/join-text/package.json (100%) rename {labs => archive/v1/labs}/extensions/join-text/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/join-text/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/join-text/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/join-text/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/join-text/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/join-text/tests/join-text.test.ts (100%) rename {labs => archive/v1/labs}/extensions/join-text/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/join-text/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/join-text/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/layer-order/README.md (100%) rename {labs => archive/v1/labs}/extensions/layer-order/package.json (100%) rename {labs => archive/v1/labs}/extensions/layer-order/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/layer-order/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/layer-order/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/layer-order/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/layer-order/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/layer-order/tests/layer-order.test.ts (100%) rename {labs => archive/v1/labs}/extensions/layer-order/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/layer-order/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/layer-order/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/limit-items/README.md (100%) rename {labs => archive/v1/labs}/extensions/limit-items/package.json (100%) rename {labs => archive/v1/labs}/extensions/limit-items/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/limit-items/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/limit-items/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/limit-items/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/limit-items/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/limit-items/tests/limit-items.test.ts (100%) rename {labs => archive/v1/labs}/extensions/limit-items/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/limit-items/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/limit-items/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/README.md (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/package.json (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/tests/live-cursors.test.ts (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/live-cursors/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/move-selected/README.md (100%) rename {labs => archive/v1/labs}/extensions/move-selected/package.json (100%) rename {labs => archive/v1/labs}/extensions/move-selected/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/move-selected/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/move-selected/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/move-selected/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/move-selected/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/move-selected/tests/move-selected.test.ts (100%) rename {labs => archive/v1/labs}/extensions/move-selected/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/move-selected/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/move-selected/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/pad-text/README.md (100%) rename {labs => archive/v1/labs}/extensions/pad-text/package.json (100%) rename {labs => archive/v1/labs}/extensions/pad-text/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/pad-text/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/pad-text/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/pad-text/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/pad-text/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/pad-text/tests/pad-text.test.ts (100%) rename {labs => archive/v1/labs}/extensions/pad-text/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/pad-text/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/pad-text/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/README.md (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/package.json (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/tests/paste-cells.test.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/paste-cells/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-special/README.md (100%) rename {labs => archive/v1/labs}/extensions/paste-special/package.json (100%) rename {labs => archive/v1/labs}/extensions/paste-special/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-special/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-special/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-special/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-special/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-special/tests/paste-special.test.ts (100%) rename {labs => archive/v1/labs}/extensions/paste-special/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/paste-special/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/paste-special/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/README.md (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/package.json (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/copy.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/guards.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/pointer.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/selection.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/transform.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/tests/patch-rebase.test.ts (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/patch-rebase/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/references/README.md (100%) rename {labs => archive/v1/labs}/extensions/references/package.json (100%) rename {labs => archive/v1/labs}/extensions/references/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/references/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/references/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/references/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/references/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/references/tests/references.test.ts (100%) rename {labs => archive/v1/labs}/extensions/references/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/references/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/references/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/README.md (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/package.json (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/tests/renumber-items.test.ts (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/renumber-items/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/round/README.md (100%) rename {labs => archive/v1/labs}/extensions/round/package.json (100%) rename {labs => archive/v1/labs}/extensions/round/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/round/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/round/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/round/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/round/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/round/tests/round.test.ts (100%) rename {labs => archive/v1/labs}/extensions/round/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/round/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/round/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/sort-items/README.md (100%) rename {labs => archive/v1/labs}/extensions/sort-items/package.json (100%) rename {labs => archive/v1/labs}/extensions/sort-items/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/sort-items/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/sort-items/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/sort-items/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/sort-items/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/sort-items/tests/sort-items.test.ts (100%) rename {labs => archive/v1/labs}/extensions/sort-items/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/sort-items/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/sort-items/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/README.md (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/package.json (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/tests/sparse-record.test.ts (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/sparse-record/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/split-text/README.md (100%) rename {labs => archive/v1/labs}/extensions/split-text/package.json (100%) rename {labs => archive/v1/labs}/extensions/split-text/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/split-text/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/split-text/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/split-text/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/split-text/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/split-text/tests/split-text.test.ts (100%) rename {labs => archive/v1/labs}/extensions/split-text/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/split-text/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/split-text/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/README.md (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/package.json (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/src/selection.ts (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/tests/stable-id-rebase.test.ts (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/stable-id-rebase/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/swap-items/README.md (100%) rename {labs => archive/v1/labs}/extensions/swap-items/package.json (100%) rename {labs => archive/v1/labs}/extensions/swap-items/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/swap-items/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/swap-items/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/swap-items/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/swap-items/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/swap-items/tests/swap-items.test.ts (100%) rename {labs => archive/v1/labs}/extensions/swap-items/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/swap-items/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/swap-items/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/README.md (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/package.json (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/tests/toggle-option.test.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/toggle-option/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/README.md (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/package.json (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/tests/toggle-value.test.ts (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/toggle-value/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/trim-text/README.md (100%) rename {labs => archive/v1/labs}/extensions/trim-text/package.json (100%) rename {labs => archive/v1/labs}/extensions/trim-text/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/trim-text/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/trim-text/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/trim-text/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/trim-text/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/trim-text/tests/trim-text.test.ts (100%) rename {labs => archive/v1/labs}/extensions/trim-text/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/trim-text/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/trim-text/vitest.config.ts (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/README.md (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/package.json (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/src/create.ts (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/src/index.ts (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/src/operations.ts (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/src/plan.ts (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/src/types.ts (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/tests/wrap-selection.test.ts (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/tsconfig.json (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/tsconfig.test.json (100%) rename {labs => archive/v1/labs}/extensions/wrap-selection/vitest.config.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/README.md (100%) rename {packages => archive/v1/packages}/bulk-edit/package.json (100%) rename {packages => archive/v1/packages}/bulk-edit/src/create.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/src/deleteAll.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/src/errors.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/src/index.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/src/matches.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/src/replaceAll.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/src/types.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/tests/bulk-edit.test.ts (100%) rename {packages => archive/v1/packages}/bulk-edit/tsconfig.json (100%) rename {packages => archive/v1/packages}/bulk-edit/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/bulk-edit/vitest.config.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/README.md (100%) rename {packages => archive/v1/packages}/clipboard-web/package.json (100%) rename {packages => archive/v1/packages}/clipboard-web/src/codec.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/src/constants.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/src/create.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/src/errors.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/src/host.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/src/index.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/src/paste.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/src/types.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/tests/clipboard-web.test.ts (100%) rename {packages => archive/v1/packages}/clipboard-web/tsconfig.json (100%) rename {packages => archive/v1/packages}/clipboard-web/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/clipboard-web/vitest.config.ts (100%) rename {packages => archive/v1/packages}/collection/README.md (100%) rename {packages => archive/v1/packages}/collection/package.json (100%) rename {packages => archive/v1/packages}/collection/src/create.ts (100%) rename {packages => archive/v1/packages}/collection/src/delete.ts (100%) rename {packages => archive/v1/packages}/collection/src/duplicate.ts (100%) rename {packages => archive/v1/packages}/collection/src/error.ts (100%) rename {packages => archive/v1/packages}/collection/src/index.ts (100%) rename {packages => archive/v1/packages}/collection/src/location.ts (100%) rename {packages => archive/v1/packages}/collection/src/move.ts (100%) rename {packages => archive/v1/packages}/collection/src/types.ts (100%) rename {packages => archive/v1/packages}/collection/tests/collection.test.ts (100%) rename {packages => archive/v1/packages}/collection/tsconfig.json (100%) rename {packages => archive/v1/packages}/collection/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/collection/vitest.config.ts (100%) rename {packages => archive/v1/packages}/comments/README.md (100%) rename {packages => archive/v1/packages}/comments/package.json (100%) rename {packages => archive/v1/packages}/comments/src/copy.ts (100%) rename {packages => archive/v1/packages}/comments/src/create.ts (100%) rename {packages => archive/v1/packages}/comments/src/errors.ts (100%) rename {packages => archive/v1/packages}/comments/src/filter.ts (100%) rename {packages => archive/v1/packages}/comments/src/index.ts (100%) rename {packages => archive/v1/packages}/comments/src/snapshot.ts (100%) rename {packages => archive/v1/packages}/comments/src/status.ts (100%) rename {packages => archive/v1/packages}/comments/src/tracking.ts (100%) rename {packages => archive/v1/packages}/comments/src/types.ts (100%) rename {packages => archive/v1/packages}/comments/src/validation.ts (100%) rename {packages => archive/v1/packages}/comments/tests/comments.test.ts (100%) rename {packages => archive/v1/packages}/comments/tsconfig.json (100%) rename {packages => archive/v1/packages}/comments/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/comments/vitest.config.ts (100%) rename {packages => archive/v1/packages}/contenteditable-react/README.md (100%) rename {packages => archive/v1/packages}/contenteditable-react/package.json (100%) rename {packages => archive/v1/packages}/contenteditable-react/src/index.ts (100%) rename {packages => archive/v1/packages}/contenteditable-react/src/useContentEditable.ts (100%) rename {packages => archive/v1/packages}/contenteditable-react/tests/contenteditable-react.test.tsx (100%) rename {packages => archive/v1/packages}/contenteditable-react/tsconfig.json (100%) rename {packages => archive/v1/packages}/contenteditable-react/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/contenteditable-react/vitest.config.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/README.md (100%) rename {packages => archive/v1/packages}/contenteditable-web/package.json (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/constants.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/core.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/dom/clipboardEvent.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/dom/createAdapter.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/dom/selection.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/dom/textProjection.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/fragment.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/index.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/src/types.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/tests/contenteditable-core.test.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/tests/contenteditable-web.test.ts (100%) rename {packages => archive/v1/packages}/contenteditable-web/tsconfig.json (100%) rename {packages => archive/v1/packages}/contenteditable-web/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/contenteditable-web/vitest.config.ts (100%) rename {packages => archive/v1/packages}/dirty-state/README.md (100%) rename {packages => archive/v1/packages}/dirty-state/package.json (100%) rename {packages => archive/v1/packages}/dirty-state/src/create.ts (100%) rename {packages => archive/v1/packages}/dirty-state/src/index.ts (100%) rename {packages => archive/v1/packages}/dirty-state/src/snapshot.ts (100%) rename {packages => archive/v1/packages}/dirty-state/src/types.ts (100%) rename {packages => archive/v1/packages}/dirty-state/tests/dirty-state.test.ts (100%) rename {packages => archive/v1/packages}/dirty-state/tsconfig.json (100%) rename {packages => archive/v1/packages}/dirty-state/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/dirty-state/vitest.config.ts (100%) rename {packages => archive/v1/packages}/form-draft/README.md (100%) rename {packages => archive/v1/packages}/form-draft/package.json (100%) rename {packages => archive/v1/packages}/form-draft/src/batch.ts (100%) rename {packages => archive/v1/packages}/form-draft/src/create.ts (100%) rename {packages => archive/v1/packages}/form-draft/src/errors.ts (100%) rename {packages => archive/v1/packages}/form-draft/src/events.ts (100%) rename {packages => archive/v1/packages}/form-draft/src/index.ts (100%) rename {packages => archive/v1/packages}/form-draft/src/json.ts (100%) rename {packages => archive/v1/packages}/form-draft/src/snapshot.ts (100%) rename {packages => archive/v1/packages}/form-draft/src/types.ts (100%) rename {packages => archive/v1/packages}/form-draft/tests/form-draft.test.ts (100%) rename {packages => archive/v1/packages}/form-draft/tsconfig.json (100%) rename {packages => archive/v1/packages}/form-draft/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/form-draft/vitest.config.ts (100%) rename {packages => archive/v1/packages}/grouping/LICENSE (100%) rename {packages => archive/v1/packages}/grouping/README.md (100%) rename {packages => archive/v1/packages}/grouping/package.json (100%) rename {packages => archive/v1/packages}/grouping/src/create.ts (100%) rename {packages => archive/v1/packages}/grouping/src/index.ts (100%) rename {packages => archive/v1/packages}/grouping/src/operations.ts (100%) rename {packages => archive/v1/packages}/grouping/src/plan.ts (100%) rename {packages => archive/v1/packages}/grouping/src/types.ts (100%) rename {packages => archive/v1/packages}/grouping/tests/grouping.test.ts (100%) rename {packages => archive/v1/packages}/grouping/tsconfig.json (100%) rename {packages => archive/v1/packages}/grouping/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/grouping/vitest.config.ts (100%) rename {packages => archive/v1/packages}/id-resolver/README.md (100%) rename {packages => archive/v1/packages}/id-resolver/package.json (100%) rename {packages => archive/v1/packages}/id-resolver/src/create.ts (100%) rename {packages => archive/v1/packages}/id-resolver/src/diagnostics.ts (100%) rename {packages => archive/v1/packages}/id-resolver/src/index.ts (100%) rename {packages => archive/v1/packages}/id-resolver/src/resolve.ts (100%) rename {packages => archive/v1/packages}/id-resolver/src/snapshot.ts (100%) rename {packages => archive/v1/packages}/id-resolver/src/types.ts (100%) rename {packages => archive/v1/packages}/id-resolver/tests/id-resolver.test.ts (100%) rename {packages => archive/v1/packages}/id-resolver/tsconfig.json (100%) rename {packages => archive/v1/packages}/id-resolver/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/id-resolver/vitest.config.ts (100%) rename {packages => archive/v1/packages}/outline/README.md (100%) rename {packages => archive/v1/packages}/outline/package.json (100%) rename {packages => archive/v1/packages}/outline/src/copy.ts (100%) rename {packages => archive/v1/packages}/outline/src/create.ts (100%) rename {packages => archive/v1/packages}/outline/src/error.ts (100%) rename {packages => archive/v1/packages}/outline/src/index.ts (100%) rename {packages => archive/v1/packages}/outline/src/move.ts (100%) rename {packages => archive/v1/packages}/outline/src/options.ts (100%) rename {packages => archive/v1/packages}/outline/src/read.ts (100%) rename {packages => archive/v1/packages}/outline/src/source.ts (100%) rename {packages => archive/v1/packages}/outline/src/types.ts (100%) rename {packages => archive/v1/packages}/outline/tests/outline.test.ts (100%) rename {packages => archive/v1/packages}/outline/tsconfig.json (100%) rename {packages => archive/v1/packages}/outline/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/outline/vitest.config.ts (100%) rename {packages => archive/v1/packages}/patch-log/README.md (100%) rename {packages => archive/v1/packages}/patch-log/package.json (100%) rename {packages => archive/v1/packages}/patch-log/src/copy.ts (100%) rename {packages => archive/v1/packages}/patch-log/src/create.ts (100%) rename {packages => archive/v1/packages}/patch-log/src/index.ts (100%) rename {packages => archive/v1/packages}/patch-log/src/replay.ts (100%) rename {packages => archive/v1/packages}/patch-log/src/store.ts (100%) rename {packages => archive/v1/packages}/patch-log/src/types.ts (100%) rename {packages => archive/v1/packages}/patch-log/tests/patch-log.test.ts (100%) rename {packages => archive/v1/packages}/patch-log/tsconfig.json (100%) rename {packages => archive/v1/packages}/patch-log/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/patch-log/vitest.config.ts (100%) rename {packages => archive/v1/packages}/patch-preview/LICENSE (100%) rename {packages => archive/v1/packages}/patch-preview/README.md (100%) rename {packages => archive/v1/packages}/patch-preview/package.json (100%) rename {packages => archive/v1/packages}/patch-preview/src/create.ts (100%) rename {packages => archive/v1/packages}/patch-preview/src/index.ts (100%) rename {packages => archive/v1/packages}/patch-preview/src/preview.ts (100%) rename {packages => archive/v1/packages}/patch-preview/src/types.ts (100%) rename {packages => archive/v1/packages}/patch-preview/tests/patch-preview.test.ts (100%) rename {packages => archive/v1/packages}/patch-preview/tsconfig.json (100%) rename {packages => archive/v1/packages}/patch-preview/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/patch-preview/vitest.config.ts (100%) rename {packages => archive/v1/packages}/persist-web/README.md (100%) rename {packages => archive/v1/packages}/persist-web/package.json (100%) rename {packages => archive/v1/packages}/persist-web/src/codec.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/constants.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/create.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/errors.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/host.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/index.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/restore.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/selection.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/snapshot.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/types.ts (100%) rename {packages => archive/v1/packages}/persist-web/src/watch.ts (100%) rename {packages => archive/v1/packages}/persist-web/tests/persist-web.test.ts (100%) rename {packages => archive/v1/packages}/persist-web/tsconfig.json (100%) rename {packages => archive/v1/packages}/persist-web/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/persist-web/vitest.config.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/README.md (100%) rename {packages => archive/v1/packages}/proposed-changes/package.json (100%) rename {packages => archive/v1/packages}/proposed-changes/src/accept.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/src/copy.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/src/create.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/src/errors.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/src/index.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/src/plan.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/src/store.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/src/types.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/tests/proposed-changes.test.ts (100%) rename {packages => archive/v1/packages}/proposed-changes/tsconfig.json (100%) rename {packages => archive/v1/packages}/proposed-changes/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/proposed-changes/vitest.config.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/README.md (100%) rename {packages => archive/v1/packages}/protected-ranges/package.json (100%) rename {packages => archive/v1/packages}/protected-ranges/src/copy.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/src/create.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/src/index.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/src/match.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/src/operations.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/src/types.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/tests/protected-ranges.test.ts (100%) rename {packages => archive/v1/packages}/protected-ranges/tsconfig.json (100%) rename {packages => archive/v1/packages}/protected-ranges/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/protected-ranges/vitest.config.ts (100%) rename {packages => archive/v1/packages}/schema-form/README.md (100%) rename {packages => archive/v1/packages}/schema-form/package.json (100%) rename {packages => archive/v1/packages}/schema-form/src/container.ts (100%) rename {packages => archive/v1/packages}/schema-form/src/create.ts (100%) rename {packages => archive/v1/packages}/schema-form/src/error.ts (100%) rename {packages => archive/v1/packages}/schema-form/src/field.ts (100%) rename {packages => archive/v1/packages}/schema-form/src/index.ts (100%) rename {packages => archive/v1/packages}/schema-form/src/tree.ts (100%) rename {packages => archive/v1/packages}/schema-form/src/types.ts (100%) rename {packages => archive/v1/packages}/schema-form/tests/schema-form.test.ts (100%) rename {packages => archive/v1/packages}/schema-form/tsconfig.json (100%) rename {packages => archive/v1/packages}/schema-form/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/schema-form/vitest.config.ts (100%) rename {packages => archive/v1/packages}/search-replace/LICENSE (100%) rename {packages => archive/v1/packages}/search-replace/README.md (100%) rename {packages => archive/v1/packages}/search-replace/package.json (100%) rename {packages => archive/v1/packages}/search-replace/src/create.ts (100%) rename {packages => archive/v1/packages}/search-replace/src/errors.ts (100%) rename {packages => archive/v1/packages}/search-replace/src/find.ts (100%) rename {packages => archive/v1/packages}/search-replace/src/index.ts (100%) rename {packages => archive/v1/packages}/search-replace/src/match.ts (100%) rename {packages => archive/v1/packages}/search-replace/src/replaceAll.ts (100%) rename {packages => archive/v1/packages}/search-replace/src/types.ts (100%) rename {packages => archive/v1/packages}/search-replace/tests/search-replace.test.ts (100%) rename {packages => archive/v1/packages}/search-replace/tsconfig.json (100%) rename {packages => archive/v1/packages}/search-replace/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/search-replace/vitest.config.ts (100%) rename {packages => archive/v1/packages}/snippets/README.md (100%) rename {packages => archive/v1/packages}/snippets/package.json (100%) rename {packages => archive/v1/packages}/snippets/src/copy.ts (100%) rename {packages => archive/v1/packages}/snippets/src/create.ts (100%) rename {packages => archive/v1/packages}/snippets/src/errors.ts (100%) rename {packages => archive/v1/packages}/snippets/src/index.ts (100%) rename {packages => archive/v1/packages}/snippets/src/insert.ts (100%) rename {packages => archive/v1/packages}/snippets/src/options.ts (100%) rename {packages => archive/v1/packages}/snippets/src/plan.ts (100%) rename {packages => archive/v1/packages}/snippets/src/store.ts (100%) rename {packages => archive/v1/packages}/snippets/src/types.ts (100%) rename {packages => archive/v1/packages}/snippets/tests/snippets.test.ts (100%) rename {packages => archive/v1/packages}/snippets/tsconfig.json (100%) rename {packages => archive/v1/packages}/snippets/tsconfig.test.json (100%) rename {packages => archive/v1/packages}/snippets/vitest.config.ts (100%) rename {scripts => archive/v1/scripts}/benchmark-adapter-readiness.mjs (100%) rename {scripts => archive/v1/scripts}/benchmark-causal.mjs (100%) rename {scripts => archive/v1/scripts}/evaluate-extension-lab.mjs (100%) rename {scripts => archive/v1/scripts}/evaluate-extensions.mjs (100%) rename {scripts => archive/v1/scripts}/run-workspace-scripts.mjs (100%) delete mode 100644 docs/generated/extensions-catalog.md create mode 100644 scripts/evaluate-archive-isolation.mjs delete mode 100644 tests/browser/outliner.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d14ff567..bdd4bb7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,9 +29,9 @@ jobs: - name: Install dependencies if: steps.scope.outputs.package_any == 'true' run: npm ci --no-audit --no-fund - - name: Verify package and official extensions + - name: Verify v2 package if: steps.scope.outputs.package_full == 'true' - run: npm run typecheck && npm test && npm run build && npm run smoke:package && npm run extensions:verify && npm run docs:evaluate + run: npm run archive:check && npm run verify -w @interactive-os/json-document && npm run standard:check && npm run docs:evaluate - name: Verify package documentation if: steps.scope.outputs.package_full != 'true' && (steps.scope.outputs.package_docs == 'true' || steps.scope.outputs.package_smoke == 'true') run: | @@ -48,33 +48,18 @@ jobs: run: npm pack -w @interactive-os/json-document --dry-run lab-extensions: - name: Lab extensions + name: Archive isolation runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - fetch-depth: 0 - uses: actions/setup-node@v6 with: node-version: "22" - - name: Detect changed scope - id: scope - env: - CI_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} - CI_HEAD_SHA: ${{ github.sha }} - run: node scripts/ci-scope.mjs - - name: Install dependencies - if: steps.scope.outputs.lab_extensions == 'true' - run: npm ci --no-audit --no-fund - - name: Verify changed lab extension dist imports - if: steps.scope.outputs.lab_extensions == 'true' - env: - LAB_EXTENSIONS_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} - LAB_EXTENSIONS_HEAD: ${{ github.sha }} - run: npm run labs:extensions:verify:changed + - name: Verify archived 1.x isolation + run: npm run archive:check playground-site: - name: Playgrounds and site + name: Site runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -90,14 +75,11 @@ jobs: CI_HEAD_SHA: ${{ github.sha }} run: node scripts/ci-scope.mjs - name: Install dependencies - if: steps.scope.outputs.site == 'true' || steps.scope.outputs.playground == 'true' || steps.scope.outputs.browser == 'true' + if: steps.scope.outputs.site == 'true' || steps.scope.outputs.browser == 'true' run: npm ci --no-audit --no-fund - - name: Verify playgrounds and site - if: steps.scope.outputs.playground == 'true' - run: npm run playground:typecheck && npm run playground:test && npm run playground:build - name: Verify site if: steps.scope.outputs.site == 'true' - run: npm run site:verify:pages + run: npm run typecheck -w @interactive-os/json-document-site && npm test -w @interactive-os/json-document-site && npm run site:verify:pages - name: Verify browser behavior if: steps.scope.outputs.browser == 'true' run: npm run browser:test diff --git a/.github/workflows/lab-extensions.yml b/.github/workflows/lab-extensions.yml index 2fe1059f..33e15a78 100644 --- a/.github/workflows/lab-extensions.yml +++ b/.github/workflows/lab-extensions.yml @@ -1,4 +1,4 @@ -name: Lab Extension Catalog +name: Archived 1.x Isolation on: workflow_dispatch: @@ -10,14 +10,12 @@ permissions: jobs: lab-extensions: - name: Full lab extension verification + name: Archive isolation runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: "22" - - name: Install dependencies - run: npm ci --no-audit --no-fund - - name: Verify all lab extension dist imports - run: npm run build && npm run labs:extensions:verify + - name: Verify archived code is outside the v2 release graph + run: npm run archive:check diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 574ec916..7dd3ed20 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,6 @@ on: push: tags: - "json-document-v*-rc.*" - - "json-document-extensions-v*-rc.*" permissions: contents: read @@ -50,87 +49,9 @@ jobs: throw new Error("publishConfig.tag must remain next for prereleases"); } ' - - name: Verify extension prerelease identity - if: startsWith(github.ref_name, 'json-document-extensions-v') - env: - RELEASE_TAG: ${{ github.ref_name }} - run: | - node --input-type=module --eval ' - import { readFileSync } from "node:fs"; - const packagePaths = [ - "packages/grouping/package.json", - "packages/patch-preview/package.json", - "packages/search-replace/package.json", - ]; - const expectedNames = [ - "@interactive-os/json-document-grouping", - "@interactive-os/json-document-patch-preview", - "@interactive-os/json-document-search-replace", - ]; - const packages = packagePaths.map((path) => - JSON.parse(readFileSync(path, "utf8")) - ); - const versions = new Set(packages.map((pkg) => pkg.version)); - if (versions.size !== 1) { - throw new Error("extension prerelease versions must match"); - } - const [version] = versions; - const expectedTag = `json-document-extensions-v${version}`; - if (process.env.RELEASE_TAG !== expectedTag) { - throw new Error(`release tag ${process.env.RELEASE_TAG} does not match ${expectedTag}`); - } - if (!/^\d+\.\d+\.\d+-rc\.\d+$/.test(version)) { - throw new Error(`extension version is not an rc prerelease: ${version}`); - } - for (const [index, pkg] of packages.entries()) { - if (pkg.name !== expectedNames[index]) { - throw new Error(`${packagePaths[index]} has unexpected name ${pkg.name}`); - } - if (pkg.publishConfig?.tag !== "next") { - throw new Error(`${pkg.name} publishConfig.tag must remain next`); - } - if (pkg.publishConfig?.access !== "public" || pkg.publishConfig?.provenance !== true) { - throw new Error(`${pkg.name} must publish publicly with provenance`); - } - if (pkg.peerDependencies?.["@interactive-os/json-document"] !== "^1.0.1 || ^1.1.0-rc.0") { - throw new Error(`${pkg.name} does not accept the core integration RC`); - } - } - ' - name: Install dependencies run: npm ci --no-audit --no-fund - - name: Build core for extension verification - if: startsWith(github.ref_name, 'json-document-extensions-v') - run: npm run build -w @interactive-os/json-document - - name: Verify extension prereleases - if: startsWith(github.ref_name, 'json-document-extensions-v') - run: | - version=$(node -p 'require("./packages/grouping/package.json").version') - for package in \ - @interactive-os/json-document-grouping \ - @interactive-os/json-document-patch-preview \ - @interactive-os/json-document-search-replace - do - if npm view "$package@$version" version >/dev/null 2>&1; then - echo "::error::$package@$version is already published" - exit 1 - fi - npm run verify -w "$package" - npm pack -w "$package" --dry-run --cache "${RUNNER_TEMP}/npm-cache" - done - name: Verify core prerelease - if: startsWith(github.ref_name, 'json-document-v') run: npm run release:check - name: Publish core prerelease - if: startsWith(github.ref_name, 'json-document-v') run: npm publish -w @interactive-os/json-document --tag next --provenance - - name: Publish extension prereleases - if: startsWith(github.ref_name, 'json-document-extensions-v') - run: | - for package in \ - @interactive-os/json-document-grouping \ - @interactive-os/json-document-patch-preview \ - @interactive-os/json-document-search-replace - do - npm publish -w "$package" --tag next --provenance - done diff --git a/README.md b/README.md index a74e8e98..b4a1ab4c 100644 --- a/README.md +++ b/README.md @@ -19,42 +19,25 @@ Pure Protocol -> Document Projection -> host adapter | 프로젝트 이해 | [docs/public/overview.md](docs/public/overview.md) | | 빠른 사용 예제 | [docs/public/quickstart.md](docs/public/quickstart.md) | | 공개 API | [docs/public/api.md](docs/public/api.md) | -| 공식 extension 사용법 | [docs/public/extensions.md](docs/public/extensions.md) | -| 제품별 feature 지도 | [docs/public/recipes.md](docs/public/recipes.md) | | 문서 구조 | [docs/README.md](docs/README.md) | | 변경 기록 | [docs/changelog.md](docs/changelog.md) | | v2 Projection 표준 | [docs/standard/v2-projection-profile.md](docs/standard/v2-projection-profile.md) | | v2 공개 표면 manifest | [docs/standard/v2-public-surface.json](docs/standard/v2-public-surface.json) | -| archived 1.x 기준선 | [docs/standard/conformance-profile.md](docs/standard/conformance-profile.md) | +| 1.x 기록 | [archive/v1/docs](archive/v1/docs) | ## 코드 지도 | 위치 | 역할 | | --- | --- | -| [packages/json-document](packages/json-document) | v2 Kernel과 archived 1.x source | -| [packages/collection](packages/collection) | ordered JSON array item 이동/복제/삭제 | -| [packages/clipboard-web](packages/clipboard-web) | browser clipboard bridge | -| [packages/contenteditable-web](packages/contenteditable-web) | `@interactive-os/json-document-contenteditable-web` DOM contenteditable text-surface adapter | -| [packages/contenteditable-react](packages/contenteditable-react) | `@interactive-os/json-document-contenteditable-react` React wrapper for contenteditable timing | -| [packages/schema-form](packages/schema-form) | schema-backed field descriptor | -| [packages/form-draft](packages/form-draft) | valid JSON commit 전 temporary invalid form input | -| [packages/protected-ranges](packages/protected-ranges) | protected JSON Pointer range edit guard | -| [packages/snippets](packages/snippets) | reusable JSON payload snippet insertion | -| [packages/dirty-state](packages/dirty-state) | clean baseline 대비 dirty state | -| [packages/bulk-edit](packages/bulk-edit) | JSONPath replace-all/delete-all | -| [packages/patch-log](packages/patch-log) | applied patch stream 기록/replay | -| [packages/persist-web](packages/persist-web) | browser storage-like persistence | -| [packages/id-resolver](packages/id-resolver) | stable id를 현재 JSON Pointer로 해석 | -| [packages/patch-preview](packages/patch-preview) | JSON Patch 적용 전 schema-safe dry-run | -| [packages/search-replace](packages/search-replace) | JSON string field 검색/치환 | -| [packages/grouping](packages/grouping) | sibling JSON item structural group/ungroup | -| [packages/proposed-changes](packages/proposed-changes) | JSON Patch 제안 accept/reject review model | -| [packages/comments](packages/comments) | JSON Pointer anchor 기반 review comment | -| [packages/outline](packages/outline) | document outline projection | -| [apps/site](apps/site) | public docs site와 workbench | -| [apps/outliner](apps/outliner) | outliner demo app | -| [apps/mobile-cms](apps/mobile-cms) | mobile CMS demo app | -| [labs/extensions](labs/extensions) | 아직 공식 package가 아닌 extension 실험 | +| [packages/json-document](packages/json-document) | 배포되는 v2 Kernel | +| [apps/site](apps/site) | v2 Core 공개 문서 사이트 | +| [archive/v1](archive/v1) | 배포·workspace·검증에서 분리된 1.x 기록 | + +이 저장소가 배포하는 package는 `@interactive-os/json-document` 하나입니다. +Selection, history, clipboard, persistence와 DOM lifecycle은 host adapter가 +여섯-member `JSONDocument` 위에서 조합합니다. DOM과 Input Events 정규화가 +필요한 제품은 별도 수명 주기를 가진 companion `@interactive-os/editable`을 +검토할 수 있으며, companion은 이 저장소의 배포물이나 v2 Core API가 아닙니다. ## 경계 diff --git a/apps/site/package.json b/apps/site/package.json index 01b86834..a3e6434a 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -10,15 +10,11 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@interactive-os/json-document-mobile-cms": "*", - "@interactive-os/json-document-outliner": "*", "react": "^19.2.5", "react-dom": "^19.2.5", "react-markdown": "^10.1.0", "rehype-slug": "^6.0.0", - "remark-gfm": "^4.0.1", - "zod": "^4.0.0", - "@interactive-os/json-document": "*" + "remark-gfm": "^4.0.1" }, "devDependencies": { "@testing-library/react": "^16.3.2", diff --git a/apps/site/src/App.tsx b/apps/site/src/App.tsx index 8d1e04c6..692445a1 100644 --- a/apps/site/src/App.tsx +++ b/apps/site/src/App.tsx @@ -7,7 +7,7 @@ type SiteRoute = { label: string; title: string; description: string; - group: "Start" | "Demos"; + group: "Start"; }; type Route = SiteRoute & { Component: ComponentType }; @@ -16,21 +16,11 @@ const SITE_URL = (import.meta.env.VITE_SITE_URL ?? "https://developer-1px.github const Docs = lazy(() => import("./routes/Docs").then((module) => ({ default: module.Docs }))); const DocsTutorial = lazy(() => import("./routes/Docs").then((module) => ({ default: module.DocsTutorial }))); const DocsApiReference = lazy(() => import("./routes/Docs").then((module) => ({ default: module.DocsApiReference }))); -const DocsExtensions = lazy(() => import("./routes/Docs").then((module) => ({ default: module.DocsExtensions }))); -const DocsRecipes = lazy(() => import("./routes/Docs").then((module) => ({ default: module.DocsRecipes }))); -const Playground = lazy(() => import("./routes/Playground").then((module) => ({ default: module.Playground }))); -const Outliner = lazy(() => import("@interactive-os/json-document-outliner").then((module) => ({ default: module.Outliner }))); -const MobileCms = lazy(() => import("@interactive-os/json-document-mobile-cms").then((module) => ({ default: module.App }))); const routeComponents: Record = { "/": Home, "/docs": Docs, "/docs/tutorial": DocsTutorial, "/docs/api": DocsApiReference, - "/docs/extensions": DocsExtensions, - "/docs/recipes": DocsRecipes, - "/playground": Playground, - "/playground/outliner": Outliner, - "/playground/mobile-cms": MobileCms, }; const ROUTES: Route[] = (siteRoutes as SiteRoute[]).map((route) => ({ ...route, @@ -148,7 +138,6 @@ export function App() { const Page = route.Component; const groupedRoutes = { Start: ROUTES.filter((item) => item.group === "Start"), - Demos: ROUTES.filter((item) => item.group === "Demos"), }; useEffect(() => { diff --git a/apps/site/src/generated/repo-catalog.ts b/apps/site/src/generated/repo-catalog.ts deleted file mode 100644 index 4e2c623f..00000000 --- a/apps/site/src/generated/repo-catalog.ts +++ /dev/null @@ -1,3261 +0,0 @@ -// Generated by scripts/generate-docs.mjs. Do not edit directly. -export const repoCatalog = { - "schemaVersion": 1, - "repo": { - "name": "@interactive-os/json-document-monorepo", - "private": true, - "summary": "json-document는 문서, 표, 슬라이드, 캔버스, 노트 편집기가 함께 쓸 수 있는\nprovider-neutral JSON 편집 protocol과 headless document projection입니다." - }, - "packages": [ - { - "path": "packages/bulk-edit", - "name": "@interactive-os/json-document-bulk-edit", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official JSONPath bulk editing extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless JSONPath bulk editing extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "apply JSONPath replace/delete operations to many document positions", - "notFor": "rendered text search UI or product workflow approval" - }, - "publicExports": [ - "BulkEdit", - "BulkEditCanReplaceAll", - "BulkEditChange", - "BulkEditChangeResult", - "BulkEditError", - "BulkEditErrorCode", - "BulkEditMatch", - "BulkEditReplaceAll", - "BulkEditResult", - "BulkEditValueMapper", - "canDeleteAll", - "canReplaceAll", - "createBulkEdit", - "deleteAll", - "replaceAll" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "bulk-edit", - "headless", - "json", - "jsonpath" - ] - }, - { - "path": "packages/clipboard-web", - "name": "@interactive-os/json-document-clipboard-web", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Web clipboard extension functions for json-document documents.", - "license": "MIT", - "summary": "Web clipboard extension functions for `@interactive-os/json-document`.", - "guidance": { - "useFor": "bridge json-document clipboard payloads to the browser clipboard", - "notFor": "TSV/CSV spreadsheet paste engines" - }, - "publicExports": [ - "CreateWebClipboardOptions", - "JSONCapabilityError", - "TextClipboardHost", - "WEB_CLIPBOARD_KIND", - "WEB_CLIPBOARD_VERSION", - "WebClipboard", - "WebClipboardCanPasteResult", - "WebClipboardCodec", - "WebClipboardCopyResult", - "WebClipboardCutResult", - "WebClipboardEnvelope", - "WebClipboardError", - "WebClipboardErrorCode", - "WebClipboardPasteResult", - "WebClipboardPayload", - "WebClipboardReadResult", - "WebClipboardWriteOk", - "WebClipboardWriteResult", - "createWebClipboard", - "defaultWebClipboardCodec" - ], - "publicExportCount": 20, - "keywords": [ - "@interactive-os/json-document", - "clipboard", - "headless", - "json", - "web" - ] - }, - { - "path": "packages/collection", - "name": "@interactive-os/json-document-collection", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official collection editing extension functions for ordered JSON array item commands in json-document documents.", - "license": "MIT", - "summary": "Official headless collection editing extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "edit ordered JSON arrays with item-level commands", - "notFor": "database collections or rendered list UI" - }, - "publicExports": [ - "Collection", - "CollectionCapabilityResult", - "CollectionDuplicateOptions", - "CollectionDuplicateResult", - "CollectionEditResult", - "CollectionError", - "CollectionErrorCode", - "CollectionSource", - "createCollection" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "collection", - "editing", - "headless", - "json" - ] - }, - { - "path": "packages/comments", - "name": "@interactive-os/json-document-comments", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official comments extension functions for review notes anchored to json-document documents.", - "license": "MIT", - "summary": "Official headless comments extension for review notes anchored to `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "anchor review comments to document structure", - "notFor": "comment UI, moderation, or author storage" - }, - "publicExports": [ - "Comment", - "CommentError", - "CommentErrorCode", - "CommentFilter", - "CommentInput", - "CommentListResult", - "CommentListener", - "CommentPointerFilter", - "CommentResult", - "CommentSnapshot", - "CommentStatus", - "CommentUpdate", - "Comments", - "createComments" - ], - "publicExportCount": 14, - "keywords": [ - "@interactive-os/json-document", - "comment", - "headless", - "review" - ] - }, - { - "path": "packages/contenteditable-react", - "name": "@interactive-os/json-document-contenteditable-react", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "React hook for the json-document contenteditable web adapter.", - "license": "MIT", - "summary": "Thin React hook for `@interactive-os/json-document-contenteditable-web`.", - "guidance": { - "useFor": "wrap the contenteditable web adapter with React render and selection restore timing", - "notFor": "rendering policy, editor commands, or non-React hosts" - }, - "publicExports": [ - "ContentEditableCommandPointerEvent", - "UseContentEditableOptions", - "UseContentEditableResult", - "useContentEditable" - ], - "publicExportCount": 4, - "keywords": [ - "@interactive-os/json-document", - "contenteditable", - "headless", - "json", - "react" - ] - }, - { - "path": "packages/contenteditable-web", - "name": "@interactive-os/json-document-contenteditable-web", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official contenteditable DOM adapter for json-document text surfaces.", - "license": "MIT", - "summary": "Official DOM adapter for using `@interactive-os/json-document` text surfaces with\nbrowser `contenteditable`.", - "guidance": { - "useFor": "bind text surfaces to DOM contenteditable selection, IME input, and clipboard events", - "notFor": "editor block semantics, toolbar commands, React rendering, or rich text schema policy" - }, - "publicExports": [ - "ContentEditableAdapter", - "ContentEditableAdapterOptions", - "ContentEditableCommand", - "ContentEditableCore", - "ContentEditableError", - "ContentEditableObservationReader", - "ContentEditableResult", - "JSON_ATOM_ATTRIBUTE", - "JSON_ATOM_REPLACEMENT", - "JSON_DOCUMENT_CONTENTEDITABLE_MIME", - "JSON_TEXT_ATTRIBUTE", - "TextSurfaceResolver", - "createContentEditableAdapter", - "createContentEditableCore" - ], - "publicExportCount": 14, - "keywords": [ - "@interactive-os/json-document", - "contenteditable", - "headless", - "json", - "web" - ] - }, - { - "path": "packages/dirty-state", - "name": "@interactive-os/json-document-dirty-state", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official dirty state tracking extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless dirty state tracking extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "compare a document to a clean baseline", - "notFor": "persistence or server save status" - }, - "publicExports": [ - "CreateDirtyStateOptions", - "DirtyState", - "DirtyStateComparator", - "DirtyStateDiscardOptions", - "DirtyStateListener", - "DirtyStateSnapshot", - "createDirtyState" - ], - "publicExportCount": 7, - "keywords": [ - "@interactive-os/json-document", - "dirty", - "editor", - "headless", - "json" - ] - }, - { - "path": "packages/form-draft", - "name": "@interactive-os/json-document-form-draft", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official form draft extension functions for temporary invalid input before committing valid json-document JSON.", - "license": "MIT", - "summary": "Official headless form draft extension for temporary input that is not ready to\nenter a schema-valid `@interactive-os/json-document` document.", - "guidance": { - "useFor": "hold temporary invalid form input before committing valid JSON", - "notFor": "rendered form components" - }, - "publicExports": [ - "CreateFormDraftOptions", - "FormDraftBatchChange", - "FormDraftBatchCommitResult", - "FormDraftBatchResult", - "FormDraftCommitResult", - "FormDraftError", - "FormDraftErrorCode", - "FormDraftListener", - "FormDraftParseContext", - "FormDraftParseResult", - "FormDraftParser", - "FormDraftSetResult", - "FormDraftSnapshot", - "FormDrafts", - "createFormDraft" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "draft", - "field", - "form", - "headless" - ] - }, - { - "path": "packages/grouping", - "name": "@interactive-os/json-document-grouping", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.1-rc.1", - "description": "Official structural group and ungroup extension functions for json-document documents.", - "license": "MIT", - "summary": "Official extension for structural `group` and `ungroup`.", - "guidance": { - "useFor": "group and ungroup selected sibling JSON items", - "notFor": "Airtable group-by views" - }, - "publicExports": [ - "Grouping", - "GroupingAdapter", - "GroupingApplyResult", - "GroupingChange", - "GroupingChangeResult", - "GroupingCreateContext", - "GroupingError", - "GroupingErrorCode", - "GroupingOperation", - "GroupingSource", - "canGroupSelection", - "canUngroupSelection", - "createGrouping", - "groupSelection", - "ungroupSelection" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "group", - "headless", - "selection", - "ungroup" - ] - }, - { - "path": "packages/id-resolver", - "name": "@interactive-os/json-document-id-resolver", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official stable id resolver extension functions for locating json-document document nodes by scope and id.", - "license": "MIT", - "summary": "Official headless stable id resolver extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "resolve scoped stable ids to current JSON Pointers", - "notFor": "id generation, relation graphs, routing, or server identity" - }, - "publicExports": [ - "IdResolver", - "IdResolverDiagnostic", - "IdResolverDiagnosticCode", - "IdResolverDocument", - "IdResolverEntry", - "IdResolverOptions", - "IdResolverScope", - "IdResolverSnapshot", - "ResolveIdErrorCode", - "ResolveIdResult", - "createIdResolver" - ], - "publicExportCount": 11, - "keywords": [ - "@interactive-os/json-document", - "editing", - "headless", - "id", - "json" - ] - }, - { - "path": "packages/json-document", - "name": "@interactive-os/json-document", - "status": "core", - "private": false, - "publishable": true, - "version": "2.0.0-rc.0", - "description": "Provider-neutral JSON editing protocol and headless document projection.", - "license": "MIT", - "summary": "문서, 표, 슬라이드, 캔버스, 노트 편집기가 함께 쓸 수 있는 provider-neutral\nJSON 편집 protocol과 headless document projection입니다.", - "guidance": null, - "publicExports": [ - "JSONAppliedChange", - "JSONCapabilityResult", - "JSONChangeMetadata", - "JSONDocument", - "JSONDocumentCommitOptions", - "JSONDocumentCommitResult", - "JSONPatchOperation", - "JSONPatchResult", - "JSONValue", - "Pointer", - "QueryResult", - "ReadResult", - "appendSegment", - "applyPatch", - "buildPointer", - "createJSONDocument", - "parentPointer", - "parsePointer", - "trackPointer", - "tryParsePointer" - ], - "publicExportCount": 20, - "keywords": [ - "document", - "editor", - "headless", - "json", - "json-patch", - "json-pointer", - "jsonpath", - "protocol" - ] - }, - { - "path": "packages/outline", - "name": "@interactive-os/json-document-outline", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official outline tree and structure editing extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless outline tree and structure editing extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "project and edit nested document outline structures", - "notFor": "Figma layer panels without a tree schema adapter" - }, - "publicExports": [ - "Outline", - "OutlineEditChange", - "OutlineEditChangeResult", - "OutlineEditError", - "OutlineEditErrorCode", - "OutlineEditResult", - "OutlineError", - "OutlineErrorCode", - "OutlineNode", - "OutlineResult", - "OutlineSource", - "OutlineStructureOptions", - "OutlineTreeOptions", - "canDemoteOutline", - "canPromoteOutline", - "createOutline", - "demoteOutline", - "promoteOutline", - "readOutline" - ], - "publicExportCount": 19, - "keywords": [ - "@interactive-os/json-document", - "headless", - "markdown", - "outline", - "outliner" - ] - }, - { - "path": "packages/patch-log", - "name": "@interactive-os/json-document-patch-log", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official patch log and replay extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless patch recording and replay extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "record and replay applied JSON Patch records", - "notFor": "product activity feeds or audit authorization" - }, - "publicExports": [ - "PatchLog", - "PatchLogEntry", - "PatchLogReplayCommitOptions", - "PatchLogReplayMetadataOption", - "PatchLogReplayMode", - "PatchLogReplayOptions", - "PatchLogReplayResult", - "PatchLogReplayStep", - "createPatchLog" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "headless", - "log", - "patch", - "replay" - ] - }, - { - "path": "packages/patch-preview", - "name": "@interactive-os/json-document-patch-preview", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.1-rc.1", - "description": "Official patch preview extension functions for dry-running json-document document patches.", - "license": "MIT", - "summary": "Official headless patch preview extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "preview patch effects before confirmation", - "notFor": "visual diff rendering" - }, - "publicExports": [ - "PatchPreview", - "PatchPreviewError", - "PatchPreviewErrorCode", - "PatchPreviewOk", - "PatchPreviewOptions", - "PatchPreviewResult", - "createPatchPreview", - "previewPatch" - ], - "publicExportCount": 8, - "keywords": [ - "@interactive-os/json-document", - "dry-run", - "headless", - "patch", - "preview" - ] - }, - { - "path": "packages/persist-web", - "name": "@interactive-os/json-document-persist-web", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official web persistence extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless web persistence extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "save and restore documents in browser storage-like hosts", - "notFor": "server sync, auth, or conflict resolution" - }, - "publicExports": [ - "CreateDocumentPersistenceOptions", - "DocumentPersistence", - "DocumentPersistenceClearOk", - "DocumentPersistenceClearResult", - "DocumentPersistenceCodec", - "DocumentPersistenceEnvelope", - "DocumentPersistenceError", - "DocumentPersistenceErrorCode", - "DocumentPersistenceHost", - "DocumentPersistenceLoadError", - "DocumentPersistencePayload", - "DocumentPersistenceRestoreOk", - "DocumentPersistenceRestoreOptions", - "DocumentPersistenceRestoreResult", - "DocumentPersistenceSaveOk", - "DocumentPersistenceSaveResult", - "DocumentPersistenceWatchEvent", - "DocumentPersistenceWatchHandle", - "DocumentPersistenceWatchOptions", - "DocumentPersistenceWatchStatus", - "WEB_PERSISTENCE_KIND", - "WEB_PERSISTENCE_VERSION", - "createDocumentPersistence", - "defaultDocumentPersistenceCodec" - ], - "publicExportCount": 24, - "keywords": [ - "@interactive-os/json-document", - "headless", - "json", - "persistence", - "web" - ] - }, - { - "path": "packages/proposed-changes", - "name": "@interactive-os/json-document-proposed-changes", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official proposed document change review extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless proposed document change review extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "review, accept, or reject proposed document patches", - "notFor": "slash commands or mention autocomplete" - }, - "publicExports": [ - "ProposedChange", - "ProposedChangeAcceptResult", - "ProposedChangeAuditData", - "ProposedChangeError", - "ProposedChangeErrorCode", - "ProposedChangeFilter", - "ProposedChangeGuard", - "ProposedChangeInput", - "ProposedChangeListener", - "ProposedChangePlan", - "ProposedChangePlanResult", - "ProposedChangeResult", - "ProposedChangeSnapshot", - "ProposedChangeStatus", - "ProposedChanges", - "ProposedChangesOptions", - "canAcceptChange", - "canProposeChange", - "createProposedChanges" - ], - "publicExportCount": 19, - "keywords": [ - "@interactive-os/json-document", - "headless", - "patch", - "proposed-changes", - "review" - ] - }, - { - "path": "packages/protected-ranges", - "name": "@interactive-os/json-document-protected-ranges", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official protected range guard extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless protected range guard extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "guard edits to protected JSON Pointer ranges", - "notFor": "2D spreadsheet selection UI or server authorization" - }, - "publicExports": [ - "ProtectedRange", - "ProtectedRangeCapabilityResult", - "ProtectedRangeEditResult", - "ProtectedRangeError", - "ProtectedRangeErrorCode", - "ProtectedRangeOperation", - "ProtectedRangePasteResult", - "ProtectedRangeSummary", - "ProtectedRanges", - "canDeleteProtectedRange", - "canInsertProtectedRange", - "canMoveProtectedRange", - "canPasteProtectedRange", - "canPatchProtectedRanges", - "canReplaceProtectedRange", - "createProtectedRanges", - "deleteProtectedRange", - "insertProtectedRange", - "moveProtectedRange", - "pasteProtectedRange", - "patchProtectedRanges", - "replaceProtectedRange" - ], - "publicExportCount": 22, - "keywords": [ - "@interactive-os/json-document", - "headless", - "lock", - "protected", - "ranges" - ] - }, - { - "path": "packages/schema-form", - "name": "@interactive-os/json-document-schema-form", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official schema-backed field descriptor extension functions for json-document documents; no rendered form UI.", - "license": "MIT", - "summary": "Official headless schema-backed field descriptor extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "derive schema-backed field descriptors", - "notFor": "form rendering or input widgets" - }, - "publicExports": [ - "SchemaFormContainerKind", - "SchemaFormError", - "SchemaFormErrorCode", - "SchemaFormField", - "SchemaFormResult", - "SchemaFormTreeField", - "SchemaFormTreeResult", - "createSchemaForm", - "createSchemaFormTree" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "field", - "form", - "headless", - "schema" - ] - }, - { - "path": "packages/search-replace", - "name": "@interactive-os/json-document-search-replace", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.1-rc.1", - "description": "Official search and replace extension functions for text fields in json-document documents.", - "license": "MIT", - "summary": "Official headless search and replace extension for text fields in `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "find and replace text across document string fields", - "notFor": "regex engines, rendered text extraction, or search UI" - }, - "publicExports": [ - "SearchReplace", - "SearchReplaceApplyResult", - "SearchReplaceChange", - "SearchReplaceChangeResult", - "SearchReplaceError", - "SearchReplaceErrorCode", - "SearchReplaceMatch", - "SearchReplaceMatchApplyResult", - "SearchReplaceMatchChange", - "SearchReplaceMatchChangeResult", - "SearchReplaceMatchTarget", - "SearchReplaceOptions", - "SearchReplaceResult", - "SearchReplaceSnapshot", - "SearchReplaceTargetFilter", - "SearchReplaceTextTarget", - "TextMatchRange", - "canReplaceAllText", - "canReplaceTextMatch", - "createSearchReplace", - "findText", - "replaceAllText", - "replaceTextMatch" - ], - "publicExportCount": 23, - "keywords": [ - "@interactive-os/json-document", - "headless", - "replace", - "search", - "text" - ] - }, - { - "path": "packages/snippets", - "name": "@interactive-os/json-document-snippets", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official snippet insertion extension functions for reusable JSON payloads in json-document documents.", - "license": "MIT", - "summary": "Official headless snippet insertion extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "insert reusable JSON payloads with schema-safe paste checks", - "notFor": "slash palette UI or snippet storage" - }, - "publicExports": [ - "Snippet", - "SnippetError", - "SnippetErrorCode", - "SnippetInsertOptions", - "SnippetInsertResult", - "SnippetPlan", - "SnippetPlanResult", - "SnippetSummary", - "Snippets", - "canInsertSnippet", - "createSnippets", - "insertSnippet" - ], - "publicExportCount": 12, - "keywords": [ - "@interactive-os/json-document", - "headless", - "insert", - "snippets", - "template" - ] - } - ], - "officialExtensions": [ - { - "path": "packages/bulk-edit", - "name": "@interactive-os/json-document-bulk-edit", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official JSONPath bulk editing extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless JSONPath bulk editing extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "apply JSONPath replace/delete operations to many document positions", - "notFor": "rendered text search UI or product workflow approval" - }, - "publicExports": [ - "BulkEdit", - "BulkEditCanReplaceAll", - "BulkEditChange", - "BulkEditChangeResult", - "BulkEditError", - "BulkEditErrorCode", - "BulkEditMatch", - "BulkEditReplaceAll", - "BulkEditResult", - "BulkEditValueMapper", - "canDeleteAll", - "canReplaceAll", - "createBulkEdit", - "deleteAll", - "replaceAll" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "bulk-edit", - "headless", - "json", - "jsonpath" - ] - }, - { - "path": "packages/clipboard-web", - "name": "@interactive-os/json-document-clipboard-web", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Web clipboard extension functions for json-document documents.", - "license": "MIT", - "summary": "Web clipboard extension functions for `@interactive-os/json-document`.", - "guidance": { - "useFor": "bridge json-document clipboard payloads to the browser clipboard", - "notFor": "TSV/CSV spreadsheet paste engines" - }, - "publicExports": [ - "CreateWebClipboardOptions", - "JSONCapabilityError", - "TextClipboardHost", - "WEB_CLIPBOARD_KIND", - "WEB_CLIPBOARD_VERSION", - "WebClipboard", - "WebClipboardCanPasteResult", - "WebClipboardCodec", - "WebClipboardCopyResult", - "WebClipboardCutResult", - "WebClipboardEnvelope", - "WebClipboardError", - "WebClipboardErrorCode", - "WebClipboardPasteResult", - "WebClipboardPayload", - "WebClipboardReadResult", - "WebClipboardWriteOk", - "WebClipboardWriteResult", - "createWebClipboard", - "defaultWebClipboardCodec" - ], - "publicExportCount": 20, - "keywords": [ - "@interactive-os/json-document", - "clipboard", - "headless", - "json", - "web" - ] - }, - { - "path": "packages/collection", - "name": "@interactive-os/json-document-collection", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official collection editing extension functions for ordered JSON array item commands in json-document documents.", - "license": "MIT", - "summary": "Official headless collection editing extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "edit ordered JSON arrays with item-level commands", - "notFor": "database collections or rendered list UI" - }, - "publicExports": [ - "Collection", - "CollectionCapabilityResult", - "CollectionDuplicateOptions", - "CollectionDuplicateResult", - "CollectionEditResult", - "CollectionError", - "CollectionErrorCode", - "CollectionSource", - "createCollection" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "collection", - "editing", - "headless", - "json" - ] - }, - { - "path": "packages/comments", - "name": "@interactive-os/json-document-comments", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official comments extension functions for review notes anchored to json-document documents.", - "license": "MIT", - "summary": "Official headless comments extension for review notes anchored to `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "anchor review comments to document structure", - "notFor": "comment UI, moderation, or author storage" - }, - "publicExports": [ - "Comment", - "CommentError", - "CommentErrorCode", - "CommentFilter", - "CommentInput", - "CommentListResult", - "CommentListener", - "CommentPointerFilter", - "CommentResult", - "CommentSnapshot", - "CommentStatus", - "CommentUpdate", - "Comments", - "createComments" - ], - "publicExportCount": 14, - "keywords": [ - "@interactive-os/json-document", - "comment", - "headless", - "review" - ] - }, - { - "path": "packages/contenteditable-react", - "name": "@interactive-os/json-document-contenteditable-react", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "React hook for the json-document contenteditable web adapter.", - "license": "MIT", - "summary": "Thin React hook for `@interactive-os/json-document-contenteditable-web`.", - "guidance": { - "useFor": "wrap the contenteditable web adapter with React render and selection restore timing", - "notFor": "rendering policy, editor commands, or non-React hosts" - }, - "publicExports": [ - "ContentEditableCommandPointerEvent", - "UseContentEditableOptions", - "UseContentEditableResult", - "useContentEditable" - ], - "publicExportCount": 4, - "keywords": [ - "@interactive-os/json-document", - "contenteditable", - "headless", - "json", - "react" - ] - }, - { - "path": "packages/contenteditable-web", - "name": "@interactive-os/json-document-contenteditable-web", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official contenteditable DOM adapter for json-document text surfaces.", - "license": "MIT", - "summary": "Official DOM adapter for using `@interactive-os/json-document` text surfaces with\nbrowser `contenteditable`.", - "guidance": { - "useFor": "bind text surfaces to DOM contenteditable selection, IME input, and clipboard events", - "notFor": "editor block semantics, toolbar commands, React rendering, or rich text schema policy" - }, - "publicExports": [ - "ContentEditableAdapter", - "ContentEditableAdapterOptions", - "ContentEditableCommand", - "ContentEditableCore", - "ContentEditableError", - "ContentEditableObservationReader", - "ContentEditableResult", - "JSON_ATOM_ATTRIBUTE", - "JSON_ATOM_REPLACEMENT", - "JSON_DOCUMENT_CONTENTEDITABLE_MIME", - "JSON_TEXT_ATTRIBUTE", - "TextSurfaceResolver", - "createContentEditableAdapter", - "createContentEditableCore" - ], - "publicExportCount": 14, - "keywords": [ - "@interactive-os/json-document", - "contenteditable", - "headless", - "json", - "web" - ] - }, - { - "path": "packages/dirty-state", - "name": "@interactive-os/json-document-dirty-state", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official dirty state tracking extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless dirty state tracking extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "compare a document to a clean baseline", - "notFor": "persistence or server save status" - }, - "publicExports": [ - "CreateDirtyStateOptions", - "DirtyState", - "DirtyStateComparator", - "DirtyStateDiscardOptions", - "DirtyStateListener", - "DirtyStateSnapshot", - "createDirtyState" - ], - "publicExportCount": 7, - "keywords": [ - "@interactive-os/json-document", - "dirty", - "editor", - "headless", - "json" - ] - }, - { - "path": "packages/form-draft", - "name": "@interactive-os/json-document-form-draft", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official form draft extension functions for temporary invalid input before committing valid json-document JSON.", - "license": "MIT", - "summary": "Official headless form draft extension for temporary input that is not ready to\nenter a schema-valid `@interactive-os/json-document` document.", - "guidance": { - "useFor": "hold temporary invalid form input before committing valid JSON", - "notFor": "rendered form components" - }, - "publicExports": [ - "CreateFormDraftOptions", - "FormDraftBatchChange", - "FormDraftBatchCommitResult", - "FormDraftBatchResult", - "FormDraftCommitResult", - "FormDraftError", - "FormDraftErrorCode", - "FormDraftListener", - "FormDraftParseContext", - "FormDraftParseResult", - "FormDraftParser", - "FormDraftSetResult", - "FormDraftSnapshot", - "FormDrafts", - "createFormDraft" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "draft", - "field", - "form", - "headless" - ] - }, - { - "path": "packages/grouping", - "name": "@interactive-os/json-document-grouping", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.1-rc.1", - "description": "Official structural group and ungroup extension functions for json-document documents.", - "license": "MIT", - "summary": "Official extension for structural `group` and `ungroup`.", - "guidance": { - "useFor": "group and ungroup selected sibling JSON items", - "notFor": "Airtable group-by views" - }, - "publicExports": [ - "Grouping", - "GroupingAdapter", - "GroupingApplyResult", - "GroupingChange", - "GroupingChangeResult", - "GroupingCreateContext", - "GroupingError", - "GroupingErrorCode", - "GroupingOperation", - "GroupingSource", - "canGroupSelection", - "canUngroupSelection", - "createGrouping", - "groupSelection", - "ungroupSelection" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "group", - "headless", - "selection", - "ungroup" - ] - }, - { - "path": "packages/id-resolver", - "name": "@interactive-os/json-document-id-resolver", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official stable id resolver extension functions for locating json-document document nodes by scope and id.", - "license": "MIT", - "summary": "Official headless stable id resolver extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "resolve scoped stable ids to current JSON Pointers", - "notFor": "id generation, relation graphs, routing, or server identity" - }, - "publicExports": [ - "IdResolver", - "IdResolverDiagnostic", - "IdResolverDiagnosticCode", - "IdResolverDocument", - "IdResolverEntry", - "IdResolverOptions", - "IdResolverScope", - "IdResolverSnapshot", - "ResolveIdErrorCode", - "ResolveIdResult", - "createIdResolver" - ], - "publicExportCount": 11, - "keywords": [ - "@interactive-os/json-document", - "editing", - "headless", - "id", - "json" - ] - }, - { - "path": "packages/outline", - "name": "@interactive-os/json-document-outline", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official outline tree and structure editing extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless outline tree and structure editing extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "project and edit nested document outline structures", - "notFor": "Figma layer panels without a tree schema adapter" - }, - "publicExports": [ - "Outline", - "OutlineEditChange", - "OutlineEditChangeResult", - "OutlineEditError", - "OutlineEditErrorCode", - "OutlineEditResult", - "OutlineError", - "OutlineErrorCode", - "OutlineNode", - "OutlineResult", - "OutlineSource", - "OutlineStructureOptions", - "OutlineTreeOptions", - "canDemoteOutline", - "canPromoteOutline", - "createOutline", - "demoteOutline", - "promoteOutline", - "readOutline" - ], - "publicExportCount": 19, - "keywords": [ - "@interactive-os/json-document", - "headless", - "markdown", - "outline", - "outliner" - ] - }, - { - "path": "packages/patch-log", - "name": "@interactive-os/json-document-patch-log", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official patch log and replay extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless patch recording and replay extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "record and replay applied JSON Patch records", - "notFor": "product activity feeds or audit authorization" - }, - "publicExports": [ - "PatchLog", - "PatchLogEntry", - "PatchLogReplayCommitOptions", - "PatchLogReplayMetadataOption", - "PatchLogReplayMode", - "PatchLogReplayOptions", - "PatchLogReplayResult", - "PatchLogReplayStep", - "createPatchLog" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "headless", - "log", - "patch", - "replay" - ] - }, - { - "path": "packages/patch-preview", - "name": "@interactive-os/json-document-patch-preview", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.1-rc.1", - "description": "Official patch preview extension functions for dry-running json-document document patches.", - "license": "MIT", - "summary": "Official headless patch preview extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "preview patch effects before confirmation", - "notFor": "visual diff rendering" - }, - "publicExports": [ - "PatchPreview", - "PatchPreviewError", - "PatchPreviewErrorCode", - "PatchPreviewOk", - "PatchPreviewOptions", - "PatchPreviewResult", - "createPatchPreview", - "previewPatch" - ], - "publicExportCount": 8, - "keywords": [ - "@interactive-os/json-document", - "dry-run", - "headless", - "patch", - "preview" - ] - }, - { - "path": "packages/persist-web", - "name": "@interactive-os/json-document-persist-web", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official web persistence extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless web persistence extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "save and restore documents in browser storage-like hosts", - "notFor": "server sync, auth, or conflict resolution" - }, - "publicExports": [ - "CreateDocumentPersistenceOptions", - "DocumentPersistence", - "DocumentPersistenceClearOk", - "DocumentPersistenceClearResult", - "DocumentPersistenceCodec", - "DocumentPersistenceEnvelope", - "DocumentPersistenceError", - "DocumentPersistenceErrorCode", - "DocumentPersistenceHost", - "DocumentPersistenceLoadError", - "DocumentPersistencePayload", - "DocumentPersistenceRestoreOk", - "DocumentPersistenceRestoreOptions", - "DocumentPersistenceRestoreResult", - "DocumentPersistenceSaveOk", - "DocumentPersistenceSaveResult", - "DocumentPersistenceWatchEvent", - "DocumentPersistenceWatchHandle", - "DocumentPersistenceWatchOptions", - "DocumentPersistenceWatchStatus", - "WEB_PERSISTENCE_KIND", - "WEB_PERSISTENCE_VERSION", - "createDocumentPersistence", - "defaultDocumentPersistenceCodec" - ], - "publicExportCount": 24, - "keywords": [ - "@interactive-os/json-document", - "headless", - "json", - "persistence", - "web" - ] - }, - { - "path": "packages/proposed-changes", - "name": "@interactive-os/json-document-proposed-changes", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official proposed document change review extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless proposed document change review extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "review, accept, or reject proposed document patches", - "notFor": "slash commands or mention autocomplete" - }, - "publicExports": [ - "ProposedChange", - "ProposedChangeAcceptResult", - "ProposedChangeAuditData", - "ProposedChangeError", - "ProposedChangeErrorCode", - "ProposedChangeFilter", - "ProposedChangeGuard", - "ProposedChangeInput", - "ProposedChangeListener", - "ProposedChangePlan", - "ProposedChangePlanResult", - "ProposedChangeResult", - "ProposedChangeSnapshot", - "ProposedChangeStatus", - "ProposedChanges", - "ProposedChangesOptions", - "canAcceptChange", - "canProposeChange", - "createProposedChanges" - ], - "publicExportCount": 19, - "keywords": [ - "@interactive-os/json-document", - "headless", - "patch", - "proposed-changes", - "review" - ] - }, - { - "path": "packages/protected-ranges", - "name": "@interactive-os/json-document-protected-ranges", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official protected range guard extension functions for json-document documents.", - "license": "MIT", - "summary": "Official headless protected range guard extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "guard edits to protected JSON Pointer ranges", - "notFor": "2D spreadsheet selection UI or server authorization" - }, - "publicExports": [ - "ProtectedRange", - "ProtectedRangeCapabilityResult", - "ProtectedRangeEditResult", - "ProtectedRangeError", - "ProtectedRangeErrorCode", - "ProtectedRangeOperation", - "ProtectedRangePasteResult", - "ProtectedRangeSummary", - "ProtectedRanges", - "canDeleteProtectedRange", - "canInsertProtectedRange", - "canMoveProtectedRange", - "canPasteProtectedRange", - "canPatchProtectedRanges", - "canReplaceProtectedRange", - "createProtectedRanges", - "deleteProtectedRange", - "insertProtectedRange", - "moveProtectedRange", - "pasteProtectedRange", - "patchProtectedRanges", - "replaceProtectedRange" - ], - "publicExportCount": 22, - "keywords": [ - "@interactive-os/json-document", - "headless", - "lock", - "protected", - "ranges" - ] - }, - { - "path": "packages/schema-form", - "name": "@interactive-os/json-document-schema-form", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official schema-backed field descriptor extension functions for json-document documents; no rendered form UI.", - "license": "MIT", - "summary": "Official headless schema-backed field descriptor extension for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "derive schema-backed field descriptors", - "notFor": "form rendering or input widgets" - }, - "publicExports": [ - "SchemaFormContainerKind", - "SchemaFormError", - "SchemaFormErrorCode", - "SchemaFormField", - "SchemaFormResult", - "SchemaFormTreeField", - "SchemaFormTreeResult", - "createSchemaForm", - "createSchemaFormTree" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "field", - "form", - "headless", - "schema" - ] - }, - { - "path": "packages/search-replace", - "name": "@interactive-os/json-document-search-replace", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.1-rc.1", - "description": "Official search and replace extension functions for text fields in json-document documents.", - "license": "MIT", - "summary": "Official headless search and replace extension for text fields in `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "find and replace text across document string fields", - "notFor": "regex engines, rendered text extraction, or search UI" - }, - "publicExports": [ - "SearchReplace", - "SearchReplaceApplyResult", - "SearchReplaceChange", - "SearchReplaceChangeResult", - "SearchReplaceError", - "SearchReplaceErrorCode", - "SearchReplaceMatch", - "SearchReplaceMatchApplyResult", - "SearchReplaceMatchChange", - "SearchReplaceMatchChangeResult", - "SearchReplaceMatchTarget", - "SearchReplaceOptions", - "SearchReplaceResult", - "SearchReplaceSnapshot", - "SearchReplaceTargetFilter", - "SearchReplaceTextTarget", - "TextMatchRange", - "canReplaceAllText", - "canReplaceTextMatch", - "createSearchReplace", - "findText", - "replaceAllText", - "replaceTextMatch" - ], - "publicExportCount": 23, - "keywords": [ - "@interactive-os/json-document", - "headless", - "replace", - "search", - "text" - ] - }, - { - "path": "packages/snippets", - "name": "@interactive-os/json-document-snippets", - "status": "official-extension", - "private": false, - "publishable": true, - "version": "0.1.0", - "description": "Official snippet insertion extension functions for reusable JSON payloads in json-document documents.", - "license": "MIT", - "summary": "Official headless snippet insertion extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "insert reusable JSON payloads with schema-safe paste checks", - "notFor": "slash palette UI or snippet storage" - }, - "publicExports": [ - "Snippet", - "SnippetError", - "SnippetErrorCode", - "SnippetInsertOptions", - "SnippetInsertResult", - "SnippetPlan", - "SnippetPlanResult", - "SnippetSummary", - "Snippets", - "canInsertSnippet", - "createSnippets", - "insertSnippet" - ], - "publicExportCount": 12, - "keywords": [ - "@interactive-os/json-document", - "headless", - "insert", - "snippets", - "template" - ] - } - ], - "labExtensions": [ - { - "path": "labs/extensions/apply-defaults", - "name": "@interactive-os/json-document-apply-defaults", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab apply-defaults (add missing object keys from defaults) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab apply-defaults extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "add missing object keys from a defaults map without overwriting existing ones", - "notFor": "filling existing empty values, removing unknown keys, or deep merge" - }, - "publicExports": [ - "ApplyDefaults", - "ApplyDefaultsChange", - "ApplyDefaultsError", - "ApplyDefaultsErrorCode", - "ApplyDefaultsResult", - "canEnsure", - "createApplyDefaults", - "ensure" - ], - "publicExportCount": 8, - "keywords": [ - "@interactive-os/json-document", - "defaults", - "ensure", - "headless", - "json", - "object" - ] - }, - { - "path": "labs/extensions/autosave", - "name": "@interactive-os/json-document-autosave", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab autosave orchestration extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab autosave extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "schedule host-owned saves after document changes", - "notFor": "retry queues, offline sync, or server conflict resolution" - }, - "publicExports": [ - "AutoSave", - "AutoSaveEvent", - "AutoSaveFlushResult", - "AutoSaveHost", - "AutoSaveHostResult", - "AutoSaveListener", - "AutoSaveOptions", - "AutoSaveReason", - "AutoSaveScheduler", - "AutoSaveSnapshot", - "AutoSaveState", - "createAutoSave" - ], - "publicExportCount": 12, - "keywords": [ - "@interactive-os/json-document", - "autosave", - "draft", - "headless" - ] - }, - { - "path": "labs/extensions/batch-update", - "name": "@interactive-os/json-document-batch-update", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab batch-update (set a field across selected items) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab batch-update extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "set a field across a list of selected item pointers to a constant or computed value", - "notFor": "selecting which items to edit, or JSONPath query-driven replacement" - }, - "publicExports": [ - "BatchUpdate", - "BatchUpdateChange", - "BatchUpdateError", - "BatchUpdateErrorCode", - "BatchUpdateOptions", - "BatchUpdateResult", - "BatchUpdateValue", - "batchUpdate", - "canBatchUpdate", - "createBatchUpdate" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "batch", - "headless", - "json", - "selection", - "set" - ] - }, - { - "path": "labs/extensions/bookmarks", - "name": "@interactive-os/json-document-bookmarks", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab bookmark tracking extension functions for json-document documents.", - "license": "MIT", - "summary": "Headless bookmark tracking extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "keep named JSON Pointer locations stable across edits", - "notFor": "browser bookmarks or route state" - }, - "publicExports": [ - "Bookmark", - "BookmarkError", - "BookmarkErrorCode", - "BookmarkSetResult", - "Bookmarks", - "BookmarksListener", - "BookmarksSnapshot", - "createBookmarks" - ], - "publicExportCount": 8, - "keywords": [ - "@interactive-os/json-document", - "bookmark", - "headless", - "pointer", - "tracking" - ] - }, - { - "path": "labs/extensions/calculated-fields", - "name": "@interactive-os/json-document-calculated-fields", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab calculated-fields (sync host-calculated fields) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab calculated field extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "sync host-computed derived JSON fields", - "notFor": "formula languages or dependency runtimes" - }, - "publicExports": [ - "CalculatedFieldChange", - "CalculatedFieldContext", - "CalculatedFieldDefinition", - "CalculatedFieldError", - "CalculatedFieldErrorCode", - "CalculatedFields", - "CalculatedFieldsChange", - "CalculatedFieldsPlanResult", - "CalculatedFieldsSyncResult", - "createCalculatedFields", - "planCalculatedFields", - "syncCalculatedFields" - ], - "publicExportCount": 12, - "keywords": [ - "@interactive-os/json-document", - "computed", - "formula", - "headless" - ] - }, - { - "path": "labs/extensions/causal-patch-inbox", - "name": "@interactive-os/json-document-causal-patch-inbox", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab causal JSON Patch inbox for json-document documents.", - "license": "MIT", - "summary": "Lab module for admitting dependency-declared JSON Patch or authored edit\nenvelopes, holding out-of-order work, and committing each envelope when it\nbecomes causally ready on one `JSONDocument`.", - "guidance": { - "useFor": "queue dependency-declared envelopes and materialize delayed positional or stable-id edits when causally ready", - "notFor": "CRDT/OT convergence, transport, persistence, or automatic conflict resolution" - }, - "publicExports": [ - "CausalAuthoredIntent", - "CausalBaseRevisionFailure", - "CausalEnvelope", - "CausalHostPublication", - "CausalHostPublicationOwnership", - "CausalHostReadyRequest", - "CausalHostReadyResult", - "CausalIntentEnvelope", - "CausalMaterializationDiagnostic", - "CausalMaterializationPolicy", - "CausalPatchDocument", - "CausalPatchEnvelope", - "CausalPatchFailure", - "CausalPatchHost", - "CausalPatchInbox", - "CausalPatchInboxOptions", - "CausalPatchInboxSnapshot", - "CausalPatchIngestError", - "CausalPatchIngestErrorCode", - "CausalPatchIngestOk", - "CausalPatchIngestResult", - "CausalPatchPublicationDocument", - "CausalPositionalIntent", - "CausalPositionalMaterializationFailure", - "CausalStableIdReplaceIntent", - "FailedCausalMaterialization", - "FailedCausalPatch", - "FaultedCausalPatch", - "QueuedCausalPatch", - "createCausalPatchInbox" - ], - "publicExportCount": 30, - "keywords": [ - "@interactive-os/json-document", - "causal", - "headless", - "inbox", - "patch" - ] - }, - { - "path": "labs/extensions/change-case", - "name": "@interactive-os/json-document-change-case", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab change-case (case/whitespace transforms on a string field) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab change-case extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "apply case/whitespace transforms (upper, lower, trim, title) to a string field", - "notFor": "locale-aware casing, rich text formatting toolbars, or find/replace" - }, - "publicExports": [ - "CaseTransform", - "ChangeCase", - "ChangeCaseChange", - "ChangeCaseError", - "ChangeCaseErrorCode", - "ChangeCaseResult", - "applyTransform", - "canTransform", - "createChangeCase" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "case", - "headless", - "json", - "text", - "transform" - ] - }, - { - "path": "labs/extensions/checkpoints", - "name": "@interactive-os/json-document-checkpoints", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab checkpoint snapshot extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab checkpoint extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "name and restore document snapshots", - "notFor": "durable version graphs or cloud backup" - }, - "publicExports": [ - "CheckpointEntry", - "CheckpointError", - "CheckpointErrorCode", - "CheckpointReadResult", - "CheckpointRestoreOptions", - "CheckpointRestoreResult", - "CheckpointSaveOptions", - "CheckpointSaveResult", - "Checkpoints", - "CheckpointsListener", - "CheckpointsSnapshot", - "CreateCheckpointsOptions", - "createCheckpoints" - ], - "publicExportCount": 13, - "keywords": [ - "@interactive-os/json-document", - "checkpoint", - "headless", - "restore", - "snapshot" - ] - }, - { - "path": "labs/extensions/clear-contents", - "name": "@interactive-os/json-document-clear-contents", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab clear-contents (reset fields to schema-derived empties) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab clear-contents extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "reset selected fields to schema-derived empty values, keeping structure", - "notFor": "structural delete, caller-supplied bulk replace, or enum/object default policy" - }, - "publicExports": [ - "ClearContents", - "ClearContentsChange", - "ClearContentsError", - "ClearContentsErrorCode", - "ClearContentsOptions", - "ClearContentsResult", - "EmptyFor", - "canClearContents", - "clearContents", - "createClearContents" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "clear", - "empty", - "headless", - "json", - "reset" - ] - }, - { - "path": "labs/extensions/convert-block-type", - "name": "@interactive-os/json-document-convert-block-type", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab block type conversion extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab extension for converting a JSON block/object from one host-described type to\nanother.", - "guidance": { - "useFor": "convert selected nodes between host-described kinds", - "notFor": "schema migration systems" - }, - "publicExports": [ - "BlockTypeConversionDescriptor", - "BlockTypeConversionError", - "BlockTypeConversionErrorCode", - "BlockTypeConversionFactoryInput", - "BlockTypeConversionInput", - "BlockTypeConversionPlan", - "BlockTypeConversionPlanResult", - "BlockTypeConversionResult", - "BlockTypeConverter", - "canConvertBlockType", - "convertBlockType", - "createBlockTypeConverter" - ], - "publicExportCount": 12, - "keywords": [ - "@interactive-os/json-document", - "convert", - "headless", - "node-kind", - "structure" - ] - }, - { - "path": "labs/extensions/convert-type", - "name": "@interactive-os/json-document-convert-type", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab convert-type (convert a field type: string/number/boolean) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab convert-type extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "convert a field type (string/number/integer/boolean) where the schema permits it", - "notFor": "locale/format-aware parsing of currency or dates, or input masks" - }, - "publicExports": [ - "ConvertType", - "ConvertTypeChange", - "ConvertTypeError", - "ConvertTypeErrorCode", - "ConvertTypeResult", - "ConvertTypeTarget", - "canConvertType", - "convertType", - "createConvertType" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "cast", - "convert-type", - "headless", - "json", - "type" - ] - }, - { - "path": "labs/extensions/dedupe", - "name": "@interactive-os/json-document-dedupe", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab dedupe (remove duplicate array items) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab dedupe extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "remove duplicate array items by whole value or a host key", - "notFor": "fuzzy matching, cross-array dedupe, or JSONPath match deletion" - }, - "publicExports": [ - "Dedupe", - "DedupeChange", - "DedupeError", - "DedupeErrorCode", - "DedupeKeyOf", - "DedupeOptions", - "DedupeResult", - "canDedupe", - "createDedupe", - "dedupe" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "array", - "dedupe", - "duplicate", - "headless", - "json" - ] - }, - { - "path": "labs/extensions/document-diff", - "name": "@interactive-os/json-document-document-diff", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab document diff and apply extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab document diff and apply extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "produce and apply patch changes toward a target document", - "notFor": "visual diff UI or merge conflict resolution" - }, - "publicExports": [ - "DocumentDiff", - "DocumentDiffApplyResult", - "DocumentDiffChange", - "DocumentDiffChangeResult", - "DocumentDiffError", - "DocumentDiffErrorCode", - "applyDocumentDiff", - "canApplyDocumentDiff", - "createDocumentDiff", - "diffDocument" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "diff", - "headless", - "json", - "patch" - ] - }, - { - "path": "labs/extensions/drag-drop", - "name": "@interactive-os/json-document-drag-drop", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab drag and drop extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab drag and drop extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "turn drag/drop intent into move or paste operations", - "notFor": "DOM drag/drop events, hit testing, or hover UI" - }, - "publicExports": [ - "DragDrop", - "DragDropError", - "DragDropErrorCode", - "DragDropInput", - "DragDropPayloadOptions", - "DragDropPerformError", - "DragDropPerformOk", - "DragDropPerformResult", - "DragDropPlan", - "DragDropPlanResult", - "DragDropSource", - "DragDropTarget", - "canDrop", - "createDragDrop", - "performDrop" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "copy", - "drag", - "drop", - "headless", - "paste" - ] - }, - { - "path": "labs/extensions/fill-blanks", - "name": "@interactive-os/json-document-fill-blanks", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab fill-blanks (fill only empty slots) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab fill-blanks extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "fill only empty slots across targets, preserving non-empty values", - "notFor": "adding missing fields, choosing targets, or unconditional batch set" - }, - "publicExports": [ - "FillBlanks", - "FillBlanksChange", - "FillBlanksError", - "FillBlanksErrorCode", - "FillBlanksOptions", - "FillBlanksResult", - "FillBlanksValue", - "canFillBlanks", - "createFillBlanks", - "fillBlanks" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "default", - "empty", - "fill", - "headless", - "json" - ] - }, - { - "path": "labs/extensions/fill-down", - "name": "@interactive-os/json-document-fill-down", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab fill-down (carry last non-empty value into blanks) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab fill-down extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "carry the last non-empty value into the empty slots that follow (ffill)", - "notFor": "constant fill, numeric series interpolation, or rendered grid UI" - }, - "publicExports": [ - "FillDown", - "FillDownChange", - "FillDownError", - "FillDownErrorCode", - "FillDownOptions", - "FillDownResult", - "canFillDown", - "createFillDown", - "fillDown" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "ffill", - "fill-down", - "headless", - "json", - "propagate" - ] - }, - { - "path": "labs/extensions/fill-series", - "name": "@interactive-os/json-document-fill-series", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab fill/series propagation extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab fill/series propagation extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "fill a value or linear series across a contiguous sibling range", - "notFor": "date/pattern series, 2D grid fill, or fill-handle drag UI" - }, - "publicExports": [ - "FillCell", - "FillGenerator", - "FillOptions", - "FillSeries", - "FillSeriesChange", - "FillSeriesError", - "FillSeriesErrorCode", - "FillSeriesResult", - "FillSource", - "canFill", - "createFillSeries", - "fill" - ], - "publicExportCount": 12, - "keywords": [ - "@interactive-os/json-document", - "autofill", - "fill", - "headless", - "json", - "series" - ] - }, - { - "path": "labs/extensions/generate-slug", - "name": "@interactive-os/json-document-generate-slug", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab generate-slug (derive a URL-safe slug from a string) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab generate-slug extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "derive a URL-safe slug from a string field (CMS title to slug)", - "notFor": "uniqueness/collision handling or non-Latin transliteration" - }, - "publicExports": [ - "GenerateSlug", - "GenerateSlugChange", - "GenerateSlugError", - "GenerateSlugErrorCode", - "GenerateSlugOptions", - "GenerateSlugResult", - "canGenerateSlug", - "createGenerateSlug", - "generateSlug" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "generate-slug", - "headless", - "json", - "slug", - "url" - ] - }, - { - "path": "labs/extensions/grid-range", - "name": "@interactive-os/json-document-grid-range", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab grid-range editing extension functions for sparse-record-backed json-document documents.", - "license": "MIT", - "summary": "Lab grid-range editing extension for sparse-record-backed `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "paste or fill rectangular grid ranges backed by sparse JSON records", - "notFor": "DOM grid selection, coordinate naming policy, formulas, or TSV/CSV parsing" - }, - "publicExports": [ - "GridRange", - "GridRangeBounds", - "GridRangeCellAction", - "GridRangeCellAddress", - "GridRangeCellIntent", - "GridRangeCellIntentKind", - "GridRangeChange", - "GridRangeDecision", - "GridRangeEqualityContext", - "GridRangeError", - "GridRangeErrorCode", - "GridRangeFillContext", - "GridRangeFillGenerator", - "GridRangeFillInput", - "GridRangeIntentContext", - "GridRangeKeyResolver", - "GridRangeOptions", - "GridRangePasteInput", - "GridRangeRect", - "GridRangeResolvedCell", - "GridRangeResult", - "GridRangeSourceCell", - "canFillGridRange", - "canPasteGridRange", - "createGridRange", - "fillGridRange", - "pasteGridRange" - ], - "publicExportCount": 27, - "keywords": [ - "@interactive-os/json-document", - "cell-range", - "fill", - "grid-range", - "headless", - "paste", - "sparse-record" - ] - }, - { - "path": "labs/extensions/increment-number", - "name": "@interactive-os/json-document-increment-number", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab increment-number (increment/decrement a numeric field) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab increment-number extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "increment, decrement, or step a numeric field with optional clamping", - "notFor": "rendered spinners, formatting, units, or currency" - }, - "publicExports": [ - "IncrementNumber", - "IncrementNumberChange", - "IncrementNumberError", - "IncrementNumberErrorCode", - "IncrementNumberOptions", - "IncrementNumberResult", - "canStep", - "createIncrementNumber", - "step" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "headless", - "increment", - "json", - "number", - "stepper" - ] - }, - { - "path": "labs/extensions/join-text", - "name": "@interactive-os/json-document-join-text", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab join-text (join an array into a string field) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab join-text extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "join an array into a string field with a separator (inverse of split-text)", - "notFor": "locale list formatting, or reading the result without writing" - }, - "publicExports": [ - "JoinText", - "JoinTextChange", - "JoinTextError", - "JoinTextErrorCode", - "JoinTextOptions", - "JoinTextResult", - "canJoin", - "createJoinText", - "join" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "array", - "headless", - "join", - "json", - "text" - ] - }, - { - "path": "labs/extensions/layer-order", - "name": "@interactive-os/json-document-layer-order", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab layer ordering extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab layer ordering extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "reorder visual stack arrays with bring/send commands", - "notFor": "canvas rendering or z-index CSS management" - }, - "publicExports": [ - "LayerOrder", - "LayerOrderAction", - "LayerOrderApplyResult", - "LayerOrderChange", - "LayerOrderChangeResult", - "LayerOrderError", - "LayerOrderErrorCode", - "LayerOrderSource", - "canReorderLayers", - "createLayerOrder", - "reorderLayers" - ], - "publicExportCount": 11, - "keywords": [ - "@interactive-os/json-document", - "headless", - "layer", - "order", - "z-order" - ] - }, - { - "path": "labs/extensions/limit-items", - "name": "@interactive-os/json-document-limit-items", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab limit-items (cap an array to N items) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab limit-items extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "cap a JSON array to at most N items, keeping the start or end", - "notFor": "choosing survivors beyond start/end, or auto-trimming on insert" - }, - "publicExports": [ - "LimitItems", - "LimitItemsChange", - "LimitItemsError", - "LimitItemsErrorCode", - "LimitItemsOptions", - "LimitItemsResult", - "canLimitItems", - "createLimitItems", - "limitItems" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "cap", - "headless", - "json", - "limit", - "slice" - ] - }, - { - "path": "labs/extensions/live-cursors", - "name": "@interactive-os/json-document-live-cursors", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab live-cursors (track remote collaborator cursors) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab package for remote collaborator cursor and selection presence.", - "guidance": { - "useFor": "track remote collaborator cursors and selections", - "notFor": "CRDT/OT or realtime transport" - }, - "publicExports": [ - "LiveCursors", - "PresenceCursor", - "PresenceCursorError", - "PresenceCursorErrorCode", - "PresenceCursorInput", - "PresenceCursorListener", - "PresenceCursorResult", - "PresenceCursorSnapshot", - "PresenceCursorUpdate", - "createLiveCursors" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "collaboration", - "cursor", - "headless", - "presence", - "selection" - ] - }, - { - "path": "labs/extensions/move-selected", - "name": "@interactive-os/json-document-move-selected", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab move-selected (move a contiguous selected block) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab move-selected extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "move a contiguous selection of sibling items to a new position", - "notFor": "single-item moves, drag/drop events, or cross-array moves" - }, - "publicExports": [ - "MoveSelected", - "MoveSelectedChange", - "MoveSelectedError", - "MoveSelectedErrorCode", - "MoveSelectedResult", - "MoveSelectedTarget", - "canMoveSelected", - "createMoveSelected", - "moveSelected" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "headless", - "json", - "move", - "reorder", - "selection" - ] - }, - { - "path": "labs/extensions/pad-text", - "name": "@interactive-os/json-document-pad-text", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab pad-text (pad a string to a minimum length) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab pad-text extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "pad a string field to a minimum length (zero-padded codes/IDs)", - "notFor": "number formatting or display-time alignment" - }, - "publicExports": [ - "PadText", - "PadTextChange", - "PadTextError", - "PadTextErrorCode", - "PadTextOptions", - "PadTextResult", - "canPadText", - "createPadText", - "padText" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "format", - "headless", - "json", - "pad", - "string" - ] - }, - { - "path": "labs/extensions/paste-cells", - "name": "@interactive-os/json-document-paste-cells", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab paste-cells (2D matrix into rectangular cells) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab paste-cells extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "paste a 2D value matrix onto a rectangular array-of-records region", - "notFor": "TSV/CSV parsing, clipboard I/O, or auto-growing the array" - }, - "publicExports": [ - "PasteCells", - "PasteCellsChange", - "PasteCellsError", - "PasteCellsErrorCode", - "PasteCellsResult", - "PasteCellsTarget", - "canPasteGrid", - "createPasteCells", - "pasteGrid" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "grid", - "headless", - "json", - "paste", - "table" - ] - }, - { - "path": "labs/extensions/paste-special", - "name": "@interactive-os/json-document-paste-special", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab paste special extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab extension for paste special.", - "guidance": { - "useFor": "adapt external payloads before schema-safe paste", - "notFor": "browser clipboard I/O or autocomplete dropdowns" - }, - "publicExports": [ - "PasteSpecial", - "PasteSpecialAdaptedPayload", - "PasteSpecialAdapter", - "PasteSpecialAdapterError", - "PasteSpecialAdapterInput", - "PasteSpecialAdapterResult", - "PasteSpecialApplyResult", - "PasteSpecialDiagnostic", - "PasteSpecialError", - "PasteSpecialErrorCode", - "PasteSpecialInput", - "PasteSpecialOptions", - "PasteSpecialPlan", - "PasteSpecialPlanResult", - "canPasteSpecial", - "createPasteSpecial", - "pasteSpecial" - ], - "publicExportCount": 17, - "keywords": [ - "@interactive-os/json-document", - "clipboard", - "compatible", - "headless", - "paste" - ] - }, - { - "path": "labs/extensions/patch-rebase", - "name": "@interactive-os/json-document-patch-rebase", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab optimistic JSON Patch rebase planner for json-document documents.", - "license": "MIT", - "summary": "Lab optimistic JSON Patch rebase planner for `@interactive-os/json-document`\ndocuments.", - "guidance": { - "useFor": "plan conservative optimistic rebases over ordered applied JSON Patch batches", - "notFor": "CRDT/OT protocols, transport, durable sync queues, or merge UI" - }, - "publicExports": [ - "RebaseChangeInput", - "RebaseChangeResult", - "RebaseConflict", - "RebaseConflictCode", - "RebaseDiagnostic", - "RebaseDiagnosticCode", - "RebaseSchema", - "rebaseChange" - ], - "publicExportCount": 8, - "keywords": [ - "@interactive-os/json-document", - "conflict", - "headless", - "patch", - "rebase" - ] - }, - { - "path": "labs/extensions/references", - "name": "@interactive-os/json-document-references", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab stable reference and backlink extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab extension for stable references and backlinks over JSON documents.", - "guidance": { - "useFor": "track stable references and backlinks over JSON documents", - "notFor": "route state or rendered links" - }, - "publicExports": [ - "ReferenceBacklinksResult", - "ReferenceDiagnostic", - "ReferenceDiagnosticCode", - "ReferenceError", - "ReferenceErrorCode", - "ReferenceFieldDescriptor", - "ReferenceLink", - "ReferenceResolveResult", - "ReferenceSetInput", - "ReferenceSetPlan", - "ReferenceSetPlanResult", - "ReferenceSetResult", - "ReferenceSnapshot", - "ReferenceTarget", - "ReferenceTargetDescriptor", - "References", - "ReferencesDescriptor", - "canSetReference", - "createReferences", - "indexReferences", - "resolveReference" - ], - "publicExportCount": 21, - "keywords": [ - "@interactive-os/json-document", - "backlinks", - "headless", - "references", - "relations" - ] - }, - { - "path": "labs/extensions/renumber-items", - "name": "@interactive-os/json-document-renumber-items", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab renumber-items (sync an order field to array position) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab renumber-items extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "sync an order/position field to each item array position after a reorder", - "notFor": "reordering the array itself, or fractional/gap indexing" - }, - "publicExports": [ - "RenumberItems", - "RenumberItemsChange", - "RenumberItemsError", - "RenumberItemsErrorCode", - "RenumberItemsOptions", - "RenumberItemsResult", - "canRenumberItems", - "createRenumberItems", - "renumberItems" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "headless", - "json", - "order", - "position", - "renumber-items" - ] - }, - { - "path": "labs/extensions/round", - "name": "@interactive-os/json-document-round", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab round (round/floor/ceil a number) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab round extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "round a number to a precision or nearest step (round/floor/ceil/trunc)", - "notFor": "currency/locale formatting, or increment/clamp (see increment-number)" - }, - "publicExports": [ - "Round", - "RoundChange", - "RoundError", - "RoundErrorCode", - "RoundMode", - "RoundOptions", - "RoundResult", - "canRound", - "createRound", - "round" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "headless", - "json", - "number", - "precision", - "round" - ] - }, - { - "path": "labs/extensions/sort-items", - "name": "@interactive-os/json-document-sort-items", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab sort-items (sort/reverse array items) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab collection sort extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "sort or reverse JSON array items", - "notFor": "query views, filters, or server sorting" - }, - "publicExports": [ - "SortItems", - "SortItemsChange", - "SortItemsChangeResult", - "SortItemsCompare", - "SortItemsError", - "SortItemsErrorCode", - "SortItemsItem", - "SortItemsResult", - "canReverse", - "canSort", - "createSortItems", - "reverse", - "sort" - ], - "publicExportCount": 13, - "keywords": [ - "@interactive-os/json-document", - "collection", - "headless", - "json", - "sort" - ] - }, - { - "path": "labs/extensions/sparse-record", - "name": "@interactive-os/json-document-sparse-record", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab sparse-record entry editing extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab sparse-record entry editing extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "set or remove keyed entries in sparse JSON records with add/replace/remove/no-op planning", - "notFor": "2D grid coordinate expansion, product key normalization, or rendered selection" - }, - "publicExports": [ - "SparseRecord", - "SparseRecordAction", - "SparseRecordChange", - "SparseRecordDecision", - "SparseRecordEdit", - "SparseRecordEqualityContext", - "SparseRecordError", - "SparseRecordErrorCode", - "SparseRecordIntent", - "SparseRecordOptions", - "SparseRecordResult", - "canEditSparseRecords", - "createSparseRecord", - "editSparseRecords" - ], - "publicExportCount": 14, - "keywords": [ - "@interactive-os/json-document", - "entries", - "headless", - "json", - "record", - "sparse-record" - ] - }, - { - "path": "labs/extensions/split-text", - "name": "@interactive-os/json-document-split-text", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab split-text (split a string into array items) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab split-text extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "split a string into array items by a delimiter (tag input, paste-as-list)", - "notFor": "CSV/TSV quoting, split-to-columns, or clipboard access" - }, - "publicExports": [ - "SplitText", - "SplitTextChange", - "SplitTextError", - "SplitTextErrorCode", - "SplitTextOptions", - "SplitTextResult", - "canSplit", - "createSplitText", - "split" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "headless", - "json", - "split", - "tags", - "text" - ] - }, - { - "path": "labs/extensions/stable-id-rebase", - "name": "@interactive-os/json-document-stable-id-rebase", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab stable-id anchored delayed change planner for json-document documents.", - "license": "MIT", - "summary": "Lab planner for materializing one delayed field replacement against the current\nPointer of a host-owned stable id.", - "guidance": { - "useFor": "materialize one delayed field replacement against a current stable-id target", - "notFor": "id policy, structural patches, CRDT/OT protocols, transport, or merge UI" - }, - "publicExports": [ - "StableIdRebaseDiagnostic", - "StableIdRebaseDocument", - "StableIdRebaseResult", - "StableIdReplaceInput", - "StableIdTarget", - "rebaseStableChange" - ], - "publicExportCount": 6, - "keywords": [ - "@interactive-os/json-document", - "conflict", - "headless", - "rebase", - "stable-id" - ] - }, - { - "path": "labs/extensions/swap-items", - "name": "@interactive-os/json-document-swap-items", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab swap-items (exchange two array items) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab swap-items extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "exchange the positions of two items in the same array", - "notFor": "cross-array swaps or moving to an arbitrary index" - }, - "publicExports": [ - "SwapItems", - "SwapItemsChange", - "SwapItemsError", - "SwapItemsErrorCode", - "SwapItemsResult", - "canSwapItems", - "createSwapItems", - "swapItems" - ], - "publicExportCount": 8, - "keywords": [ - "@interactive-os/json-document", - "exchange", - "headless", - "json", - "reorder", - "swap-items" - ] - }, - { - "path": "labs/extensions/toggle-option", - "name": "@interactive-os/json-document-toggle-option", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab toggle-option (toggle a value in an array set) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab toggle-option extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "toggle, add, or remove a value's presence in a JSON array (tag/multi-select)", - "notFor": "ordered insertion position or deduping an existing array" - }, - "publicExports": [ - "MembershipAction", - "ToggleOption", - "ToggleOptionChange", - "ToggleOptionError", - "ToggleOptionErrorCode", - "ToggleOptionOptions", - "ToggleOptionResult", - "createToggleOption", - "plan" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "headless", - "json", - "membership", - "set", - "toggle" - ] - }, - { - "path": "labs/extensions/toggle-value", - "name": "@interactive-os/json-document-toggle-value", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab toggle-value (toggle/advance a field through values) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab toggle-value extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "toggle a boolean or advance an enum/value field (enum options come from the schema)", - "notFor": "rendered toggle controls or keyboard policy" - }, - "publicExports": [ - "ToggleValue", - "ToggleValueChange", - "ToggleValueDirection", - "ToggleValueError", - "ToggleValueErrorCode", - "ToggleValueOptions", - "ToggleValueResult", - "canToggleValue", - "createToggleValue", - "toggleValue" - ], - "publicExportCount": 10, - "keywords": [ - "@interactive-os/json-document", - "enum", - "headless", - "json", - "toggle", - "toggle-value" - ] - }, - { - "path": "labs/extensions/trim-text", - "name": "@interactive-os/json-document-trim-text", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab trim-text (cap a string to a max length) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab trim-text extension for `@interactive-os/json-document` documents.", - "guidance": { - "useFor": "cap a string field to a max length with optional ellipsis and word boundary", - "notFor": "display-time CSS truncation or grapheme/locale-aware length" - }, - "publicExports": [ - "TrimText", - "TrimTextChange", - "TrimTextError", - "TrimTextErrorCode", - "TrimTextOptions", - "TrimTextResult", - "canTrimText", - "createTrimText", - "trimText" - ], - "publicExportCount": 9, - "keywords": [ - "@interactive-os/json-document", - "excerpt", - "headless", - "json", - "text", - "trim-text" - ] - }, - { - "path": "labs/extensions/wrap-selection", - "name": "@interactive-os/json-document-wrap-selection", - "status": "lab-extension", - "private": true, - "publishable": false, - "version": "0.1.0", - "description": "Lab wrap-selection (wrap/unwrap selected items) extension functions for json-document documents.", - "license": "MIT", - "summary": "Lab extension for structural `wrap` and `unwrap`.", - "guidance": { - "useFor": "wrap sibling JSON items in host-defined containers", - "notFor": "visual grouping or layout containers" - }, - "publicExports": [ - "WrapCreateContext", - "WrapSelection", - "WrapSelectionAdapter", - "WrapSelectionApplyResult", - "WrapSelectionChange", - "WrapSelectionChangeResult", - "WrapSelectionError", - "WrapSelectionErrorCode", - "WrapSelectionOperation", - "WrapSource", - "canUnwrapSelection", - "canWrapSelection", - "createWrapSelection", - "unwrapSelection", - "wrapSelection" - ], - "publicExportCount": 15, - "keywords": [ - "@interactive-os/json-document", - "headless", - "structure", - "unwrap", - "wrap" - ] - } - ], - "apps": [ - { - "path": "apps/copy-review-lab", - "name": "@interactive-os/json-document-copy-review-lab", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": "Dogfoods `@interactive-os/json-document-search-replace` as a copy cleanup product surface.", - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/grouping-lab", - "name": "@interactive-os/json-document-grouping-lab", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": "Dogfoods `@interactive-os/json-document-grouping` as a small structural editor feature.", - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/mobile-cms", - "name": "@interactive-os/json-document-mobile-cms", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": null, - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/outliner", - "name": "@interactive-os/json-document-outliner", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": null, - "guidance": null, - "publicExports": [ - "EMPTY_NODE", - "OutlineNode", - "OutlineSchema", - "Outliner", - "SAMPLE" - ], - "publicExportCount": 5, - "keywords": [] - }, - { - "path": "apps/outliner-blind-a", - "name": "@interactive-os/json-document-outliner-blind-a", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": null, - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/outliner-blind-b", - "name": "@interactive-os/json-document-outliner-blind-b", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": null, - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/proposed-changes-lab", - "name": "@interactive-os/json-document-proposed-changes-lab", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": "Dogfoods `@interactive-os/json-document-proposed-changes` as a small proposed document change review feature.", - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/protected-ranges-lab", - "name": "@interactive-os/json-document-protected-ranges-lab", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": "Dogfoods official `@interactive-os/json-document-protected-ranges` as a locked structured content editor.", - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/review-comments-lab", - "name": "@interactive-os/json-document-review-comments-lab", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": "Dogfoods `@interactive-os/json-document-comments` as an editorial review product surface.", - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/schema-form-lab", - "name": "@interactive-os/json-document-schema-form-lab", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": null, - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/site", - "name": "@interactive-os/json-document-site", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": null, - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - }, - { - "path": "apps/snippet-composer-lab", - "name": "@interactive-os/json-document-snippet-composer-lab", - "status": "app", - "private": true, - "publishable": false, - "version": null, - "description": null, - "license": null, - "summary": "Dogfoods official `@interactive-os/json-document-snippets` as a page block composer.", - "guidance": null, - "publicExports": [], - "publicExportCount": 0, - "keywords": [] - } - ], - "totals": { - "packages": 20, - "officialExtensions": 19, - "labExtensions": 41, - "apps": 12 - } -} as const; - -export type RepoCatalog = typeof repoCatalog; diff --git a/apps/site/src/playgrounds/InterfaceWorkbench.playground.tsx b/apps/site/src/playgrounds/InterfaceWorkbench.playground.tsx deleted file mode 100644 index 1ba675a2..00000000 --- a/apps/site/src/playgrounds/InterfaceWorkbench.playground.tsx +++ /dev/null @@ -1,1909 +0,0 @@ -import { useEffect, useMemo, useState, type ReactNode } from "react"; -import { z } from "zod"; -import { - appendSegment, - applyOperation, - applyPatch, - applyPatchToTrustedState, - buildPointer, - createJSONDocument, - escapeSegment, - JSONDocumentError, - lastSegment, - lastSegmentIndex, - parentPointer, - parsePointer, - PointerSyntaxError, - trackPointer, - tryParsePointer, - unescapeSegment, - withLastSegment, - type JSONPatchOperation, - type JSONCapabilityResult, - type Pointer, - type SelectionSnap, -} from "@interactive-os/json-document/session"; -import { useJSONDocument } from "@interactive-os/json-document/react"; - -const Card = z.object({ - id: z.string(), - title: z.string().min(1), - status: z.enum(["todo", "doing", "done"]), - points: z.number().int().min(0).max(13), - tags: z.array(z.string().min(1)), -}); - -const BoardSchema = z.object({ - title: z.string().min(1), - settings: z.object({ - archived: z.boolean(), - owner: z.string().min(1), - }), - lists: z.array(z.object({ - id: z.string(), - name: z.string().min(1), - cards: z.array(Card), - })), -}); - -type Board = z.infer; -type BenchResult = { - call: string; - value: unknown; - feature?: string; - bindings?: readonly string[]; - effect?: readonly string[]; -}; -type FeatureStageId = - | "board-setup" - | "card-intake" - | "card-edit" - | "flow-columns" - | "selection-bulk" - | "clipboard-reuse" - | "find-filter" - | "recovery-history" - | "integration"; - -type FeatureStage = { - id: FeatureStageId; - title: string; - apis: readonly string[]; -}; - -const featureStages: readonly FeatureStage[] = [ - { id: "board-setup", title: "Board setup", apis: ["useJSONDocument", "createJSONDocument", "doc.load", "doc.reset", "doc.subscribe"] }, - { id: "card-intake", title: "Card intake", apis: ["schema.accepts", "schema.describe", "canInsert", "insert"] }, - { id: "card-edit", title: "Card edit", apis: ["at", "exists", "schema.kind", "canReplace", "replace", "commit", "canDelete", "delete"] }, - { id: "flow-columns", title: "Flow across columns", apis: ["canMove", "move", "canDuplicate", "duplicate"] }, - { id: "selection-bulk", title: "Selection and bulk work", apis: ["selection.*", "canDelete", "delete"] }, - { id: "clipboard-reuse", title: "Reuse via clipboard", apis: ["clipboard.*", "canCopy", "canCut", "canPaste", "paste"] }, - { id: "find-filter", title: "Find and filter", apis: ["canFind", "find", "selection.selectRanges"] }, - { id: "recovery-history", title: "Recovery and history", apis: ["canUndo", "canRedo", "history.*"] }, - { id: "integration", title: "Integration and helpers", apis: ["applyOperation", "applyPatch", "applyPatchToTrustedState", "trackPointer", "pointer helpers"] }, -]; - -const initialBoard: Board = { - title: "Workbench board", - settings: { archived: false, owner: "core" }, - lists: [ - { - id: "todo", - name: "Todo", - cards: [ - { id: "c1", title: "Patch API", status: "todo", points: 2, tags: ["ops"] }, - { id: "c2", title: "Selection API", status: "todo", points: 3, tags: ["selection"] }, - ], - }, - { - id: "doing", - name: "Doing", - cards: [ - { id: "c3", title: "Clipboard API", status: "doing", points: 5, tags: ["clipboard"] }, - ], - }, - { - id: "done", - name: "Done", - cards: [ - { id: "c4", title: "History API", status: "done", points: 8, tags: ["history"] }, - ], - }, - ], -}; - -const sampleCard: Board["lists"][number]["cards"][number] = { - id: "new", - title: "Inserted card", - status: "todo", - points: 1, - tags: ["new"], -}; - -const invalidCard = { - id: "bad", - title: "", - status: "blocked", - points: -1, - tags: [], -}; - -const publicTypeExports = [ - "HistoryTransactionOptions", - "JSONCapabilityResult", - "JSONChangeMetadata", - "JSONDocument", - "JSONDocumentCommitOptions", - "JSONDocumentDuplicateError", - "JSONDocumentDuplicateOptions", - "JSONDocumentDuplicateResult", - "JSONDocumentHistory", - "JSONDocumentOptions", - "JSONDocumentPasteOptions", - "JSONDocumentPasteTarget", - "JSONPatchInput", - "JSONPatchOperation", - "SelectionPoint", - "JSONResult", - "Pointer", - "ClipboardCopyOptions", - "ClipboardCopyError", - "ClipboardCopyOk", - "ClipboardCopyResult", - "ClipboardCutError", - "ClipboardCutOk", - "ClipboardCutOptions", - "ClipboardCutResult", - "ClipboardEmpty", - "ClipboardMutationOk", - "ClipboardPasteDiscriminatorMismatch", - "ClipboardPasteError", - "ClipboardPasteResult", - "ClipboardReadOk", - "ClipboardReadOptions", - "ClipboardReadResult", - "ClipboardState", - "ClipboardWriteOptions", - "EntriesResult", - "EntryKind", - "QueryResult", - "ReadEntry", - "ReadResult", - "SchemaDescription", - "SchemaDescriptionResult", - "SchemaErrorCode", - "SchemaErrorResult", - "SchemaKind", - "SchemaKindResult", - "SchemaPathMode", - "SchemaQueryResult", - "SchemaState", - "SelectionOptions", - "SelectionPointObject", - "SelectionOrderedRange", - "SelectionOrderedRangeEntry", - "SelectionAffinity", - "SelectionContext", - "SelectionCursorDirection", - "SelectionCursorErrorCode", - "SelectionCursorOptions", - "SelectionCursorResult", - "SelectionCursorTarget", - "SelectionDirection", - "SelectionEdge", - "SelectionMode", - "SelectionOrderErrorCode", - "SelectionOrderOptions", - "SelectionPointOrderResult", - "SelectionPointerSpan", - "SelectionPointerSpansResult", - "SelectionRange", - "SelectionRangeInput", - "SelectionRangeOrderResult", - "SelectionRangesOrderResult", - "SelectionScopeErrorCode", - "SelectionScopeOptions", - "SelectionScopeResult", - "SelectionScopeTarget", - "SelectionSnap", - "SelectionSource", - "SelectionSpanOptions", - "SelectionState", - "SelectionType", - "DeleteSelectionTextResult", - "ReplaceSelectionTextResult", - "SelectionTextDeleteDirection", - "SelectionTextDeleteOptions", - "SelectionTextEdit", - "SelectionTextEditErrorCode", - "SelectionTextEditOptions", - "SelectionTextEditsResult", - "ClipboardSource", -]; - -const publicTypeGroups = [ - { - label: "Document and patch", - note: "used by Add, Edit, Move, Duplicate", - items: [ - "JSONDocument", - "JSONPatchInput", - "JSONPatchOperation", - "JSONResult", - "JSONCapabilityResult", - "JSONChangeMetadata", - "Pointer", - ], - }, - { - label: "History", - note: "used by Undo and redo", - items: [ - "JSONDocumentHistory", - "JSONDocumentCommitOptions", - "HistoryTransactionOptions", - ], - }, - { - label: "Selection", - note: "used by Find and Bulk cards", - items: publicTypeExports.filter((item) => item.includes("Selection") || item === "SelectionPoint" || item === "SelectionPointObject" || item === "SelectionOrderedRange" || item === "SelectionOrderedRangeEntry"), - }, - { - label: "Clipboard", - note: "used by Copy and paste", - items: publicTypeExports.filter((item) => item.includes("Clipboard") || item.includes("Copy") || item.includes("Cut") || item.includes("Paste") || item.includes("Duplicate")), - }, - { - label: "Schema and read", - note: "used by Read schema", - items: publicTypeExports.filter((item) => item.includes("Schema") || item.includes("Read") || item.includes("Entries") || item.includes("Query") || item === "EntryKind"), - }, -]; - -function cardPointer(listIndex: number, cardIndex: number): Pointer { - return `/lists/${listIndex}/cards/${cardIndex}` as Pointer; -} - -function collapsedSelection(pointer: Pointer): SelectionSnap { - return { - selectedPointers: [pointer], - selectionRanges: [{ anchor: pointer, focus: pointer }], - primaryIndex: 0, - anchor: pointer, - focus: pointer, - }; -} - -function stringify(value: unknown): string { - return JSON.stringify(value, null, 2); -} - -function parseJson(value: string): { ok: true; value: unknown } | { ok: false; message: string } { - try { - return { ok: true, value: JSON.parse(value) }; - } catch (error) { - return { ok: false, message: error instanceof Error ? error.message : "invalid JSON" }; - } -} - -function parseNumberPayload(value: string): unknown { - const trimmed = value.trim(); - if (trimmed.length === 0) return { __invalid_number: "empty" }; - const number = Number(trimmed); - return Number.isFinite(number) ? number : { __invalid_number: value }; -} - -function canDisabledReason(result: JSONCapabilityResult): string | undefined { - if (result.ok) return undefined; - const violation = result.violations?.[0]; - const detail = result.reason - ?? (violation ? `${violation.path}: ${violation.message}` : undefined) - ?? result.pointer; - return detail ? `can: ${result.code}: ${detail}` : `can: ${result.code}`; -} - -function capabilityStatus(result: JSONCapabilityResult): string { - return result.ok ? "ok" : result.code; -} - -function disabledMark(reason: string | undefined): string { - if (!reason) return "cannot"; - if (reason.startsWith("state: ")) return `cannot ${reason.slice("state: ".length)}`; - if (!reason.startsWith("can: ")) return "cannot"; - const code = reason.slice("can: ".length).split(":")[0]?.trim().split(" ")[0]; - return code ? `cannot ${code}` : "cannot"; -} - -function stateStatus(reason: string | undefined): string | undefined { - if (!reason) return undefined; - if (reason.startsWith("state: ")) return `state ${reason.slice("state: ".length)}`; - if (!reason.startsWith("can: ")) return reason; - const code = reason.slice("can: ".length).split(":")[0]?.trim().split(" ")[0]; - return code ? `can ${code}` : "can blocked"; -} - -function isEditableShortcutTarget(target: EventTarget | null): boolean { - if (!(target instanceof HTMLElement)) return false; - if (target.isContentEditable) return true; - return target instanceof HTMLInputElement - || target instanceof HTMLTextAreaElement - || target instanceof HTMLSelectElement; -} - -function resultSummary(value: unknown, call = ""): string[] { - if (value === null || typeof value !== "object") return [String(value)]; - const record = value as Record; - const lines: string[] = []; - if (typeof record.ok === "boolean") { - lines.push(record.ok ? "ok true" : `ok false ${String(record.code ?? "")}`.trim()); - } - if (Array.isArray(record.applied)) { - lines.push(...record.applied.slice(0, 3).map((operation) => { - const op = operation as { op?: unknown; path?: unknown; from?: unknown }; - return [op.op, op.from ? `from ${String(op.from)}` : null, op.path ? `path ${String(op.path)}` : null] - .filter(Boolean) - .join(" "); - })); - } - if (typeof record.duplicatedTo === "string") lines.push(`duplicatedTo ${record.duplicatedTo}`); - if (typeof record.pointer === "string") lines.push(`pointer ${record.pointer}`); - if (typeof record.kind === "string") lines.push(`kind ${record.kind}`); - if (typeof record.type === "string") lines.push(`type ${record.type}`); - if (typeof record.path === "string") lines.push(`path ${record.path}`); - if (Array.isArray(record.entries)) lines.push(`entries ${record.entries.length}`); - if (Array.isArray(record.pointers)) lines.push(`pointers ${record.pointers.length}`); - if (Array.isArray(record.selectedPointers)) lines.push(`selected ${record.selectedPointers.length}`); - if (Array.isArray(record.events)) lines.push(`events ${record.events.length}`); - if (Array.isArray(record.patch)) lines.push(`patch ${record.patch.length}`); - if (Array.isArray(record.parsePointer)) lines.push(`segments ${record.parsePointer.join("/")}`); - if (typeof record.buildPointer === "string") lines.push(`buildPointer ${record.buildPointer}`); - if (call.includes("schema.describe")) lines.push("schema insert"); - if (call.includes("clipboard.clear") && record.hasData === false) lines.push("clipboard empty"); - if (call.includes("selection?.textPatch")) lines.push("text patch"); - if (Array.isArray(record.violations) && record.violations.length > 0) { - const first = record.violations[0] as { path?: unknown; message?: unknown }; - lines.push(`violation ${String(first.path ?? "")} ${String(first.message ?? "")}`.trim()); - } - if ("value" in record) lines.push(...valueSummary(record.value)); - return lines.length > 0 ? lines : ["object returned"]; -} - -function valueSummary(value: unknown): string[] { - if (value === null) return ["value null"]; - if (Array.isArray(value)) return [`array ${value.length}`]; - if (typeof value !== "object") return [`value ${String(value)}`]; - const record = value as Record; - if (typeof record.title === "string" && typeof record.status === "string") return [`card "${record.title}"`]; - if (typeof record.title === "string") return [`title "${record.title}"`]; - if (typeof record.name === "string") return [`name "${record.name}"`]; - const keys = Object.keys(record); - return keys.length > 0 ? [`value keys ${keys.slice(0, 3).join(",")}`] : ["value object"]; -} - -function isBoard(value: unknown): value is Board { - return BoardSchema.safeParse(value).success; -} - -function resultBoard(value: unknown, fallback: Board): Board { - if (isBoard(value)) return value; - if (value !== null && typeof value === "object" && "value" in value) { - const next = (value as { value?: unknown }).value; - if (isBoard(next)) return next; - } - return fallback; -} - -function operationEffects(value: unknown): string[] { - if (value === null || typeof value !== "object") return []; - const record = value as Record; - const effects: string[] = []; - if (typeof record.duplicatedTo === "string") effects.push(`duplicated to ${record.duplicatedTo}`); - if (!Array.isArray(record.applied)) return effects; - - for (const operation of record.applied.slice(0, 4)) { - const op = operation as { op?: unknown; path?: unknown; from?: unknown }; - const path = typeof op.path === "string" ? op.path : ""; - const from = typeof op.from === "string" ? op.from : ""; - if (op.op === "add") effects.push(`added ${path}`); - else if (op.op === "remove") effects.push(`removed ${path}`); - else if (op.op === "replace") effects.push(`replaced ${path}`); - else if (op.op === "move") effects.push(`moved ${from} -> ${path}`); - else if (op.op === "copy") effects.push(`copied ${from} -> ${path}`); - else if (typeof op.op === "string") effects.push(`${op.op} ${path}`.trim()); - } - return effects; -} - -function boardEffect(before: Board, after: Board): string[] { - const effects: string[] = []; - const beforeCounts = before.lists.map((list) => list.cards.length).join("/"); - const afterCounts = after.lists.map((list) => list.cards.length).join("/"); - if (beforeCounts !== afterCounts) effects.push(`cards ${beforeCounts} -> ${afterCounts}`); - - const beforeCards = flattenCards(before); - const afterCards = flattenCards(after); - const beforeById = new Map(beforeCards.map((item) => [item.card.id, item])); - const afterById = new Map(afterCards.map((item) => [item.card.id, item])); - - if (afterCards.length < beforeCards.length) { - for (const item of beforeCards) { - if (!afterById.has(item.card.id)) { - effects.push(`removed ${item.card.title} ${item.pointer}`); - return effects; - } - } - const index = firstDifferentCardIndex(beforeCards, afterCards); - const removed = beforeCards[index] ?? beforeCards[beforeCards.length - 1]; - if (removed) { - effects.push(`removed ${removed.card.title} ${removed.pointer}`); - return effects; - } - } - - if (afterCards.length > beforeCards.length) { - for (const item of afterCards) { - if (!beforeById.has(item.card.id)) { - effects.push(`added ${item.card.title} ${item.pointer}`); - return effects; - } - } - const index = firstDifferentCardIndex(afterCards, beforeCards); - const added = afterCards[index] ?? afterCards[afterCards.length - 1]; - if (added) { - effects.push(`added ${added.card.title} ${added.pointer}`); - return effects; - } - } - - for (const item of beforeCards) { - const next = afterById.get(item.card.id); - if (next && next.pointer !== item.pointer) { - effects.push(`moved ${item.card.title} ${item.pointer} -> ${next.pointer}`); - return effects; - } - } - - for (let listIndex = 0; listIndex < Math.min(before.lists.length, after.lists.length); listIndex += 1) { - const beforeCards = before.lists[listIndex]?.cards ?? []; - const afterCards = after.lists[listIndex]?.cards ?? []; - for (let cardIndex = 0; cardIndex < Math.min(beforeCards.length, afterCards.length); cardIndex += 1) { - const beforeCard = beforeCards[cardIndex]; - const afterCard = afterCards[cardIndex]; - if (beforeCard && afterCard && beforeCard.id === afterCard.id && beforeCard.title !== afterCard.title) { - effects.push(`title ${cardPointer(listIndex, cardIndex)}: ${beforeCard.title} -> ${afterCard.title}`); - return effects; - } - } - } - return effects; -} - -function flattenCards(board: Board): Array<{ - pointer: Pointer; - card: Board["lists"][number]["cards"][number]; -}> { - return board.lists.flatMap((list, listIndex) => - list.cards.map((card, cardIndex) => ({ - pointer: cardPointer(listIndex, cardIndex), - card, - })), - ); -} - -function firstDifferentCardIndex( - left: Array<{ card: Board["lists"][number]["cards"][number] }>, - right: Array<{ card: Board["lists"][number]["cards"][number] }>, -): number { - const length = Math.max(left.length, right.length); - for (let index = 0; index < length; index += 1) { - const leftCard = left[index]?.card; - const rightCard = right[index]?.card; - if (!leftCard || !rightCard || leftCard.id !== rightCard.id || leftCard.title !== rightCard.title) { - return index; - } - } - return Math.max(0, left.length - 1); -} - -function cardTitleAt(board: Board, pointer: Pointer): string | null { - const segments = tryParsePointer(pointer); - if (!segments || segments.length !== 4 || segments[0] !== "lists" || segments[2] !== "cards") return null; - const listIndex = Number(segments[1]); - const cardIndex = Number(segments[3]); - return board.lists[listIndex]?.cards[cardIndex]?.title ?? null; -} - -function mutatesBoard(call: string): boolean { - return call.includes("doc.patch(") - || call.includes("doc.replace(") - || call.includes("doc.delete(") - || call.includes("doc.move(") - || call.includes("doc.commit(") - || call.includes("doc.duplicate(") - || call.includes("doc.cut(") - || call.includes("doc.paste(") - || call.includes("doc.undo(") - || call.includes("doc.redo(") - || call.includes("doc.history.transaction("); -} - -function selectedLabel(selected: readonly string[]): string { - return selected.length === 0 ? "none" : selected.join(", "); -} - -function cardRekey() { - return { fields: ["id"], strategy: "suffix" as const }; -} - -function columnClass(id: string): string { - if (id === "doing") return "border-amber-200 bg-amber-50/70"; - if (id === "done") return "border-emerald-200 bg-emerald-50/70"; - return "border-stone-200 bg-stone-50"; -} - -function statusClass(status: Board["lists"][number]["cards"][number]["status"]): string { - if (status === "doing") return "bg-amber-100 text-amber-800"; - if (status === "done") return "bg-emerald-100 text-emerald-800"; - return "bg-sky-100 text-sky-800"; -} - -function statusForInsertTarget(board: Board, target: Pointer): Board["lists"][number]["cards"][number]["status"] | null { - const segments = tryParsePointer(target); - if (!segments || segments[0] !== "lists" || segments[2] !== "cards") return null; - const listIndex = Number(segments[1]); - const listId = board.lists[listIndex]?.id; - if (listId === "todo" || listId === "doing" || listId === "done") return listId; - return null; -} - -function payloadWithStatus(payloadText: string, status: Board["lists"][number]["cards"][number]["status"]): string { - const parsed = parseJson(payloadText); - if (!parsed.ok || parsed.value === null || typeof parsed.value !== "object" || Array.isArray(parsed.value)) { - return payloadText; - } - return stringify({ ...parsed.value, status }); -} - -export function InterfaceWorkbench() { - const doc = useJSONDocument(BoardSchema, initialBoard, { - history: 100, - strict: false, - selection: { mode: "extended", initial: [cardPointer(0, 0)] }, - }); - const selectedPointers = doc.selection?.selectedPointers ?? []; - const primaryPointer = doc.selection?.primaryPointer ?? null; - const [valueTarget, setValueTarget] = useState(cardPointer(0, 0)); - const [insertTarget, setInsertTarget] = useState("/lists/0/cards/-" as Pointer); - const [query, setQuery] = useState("$..cards[?(@.status=='todo')]"); - const [payload, setPayload] = useState(stringify(sampleCard)); - const [textPayload, setTextPayload] = useState("Patch edit"); - const [pointsPayload, setPointsPayload] = useState("8"); - const [badPointsPayload, setBadPointsPayload] = useState("-5"); - const [result, setResult] = useState({ call: "ready", value: doc.value }); - const [featureResults, setFeatureResults] = useState>({}); - const [apiCoverageOpen, setApiCoverageOpen] = useState(false); - const [activeStageId, setActiveStageId] = useState("board-setup"); - - const pointers = useMemo( - () => doc.value.lists.flatMap((list, listIndex) => - list.cards.map((card, cardIndex) => ({ - pointer: cardPointer(listIndex, cardIndex), - card, - })), - ), - [doc.value], - ); - const insertPointers = useMemo( - () => doc.value.lists.map((list, listIndex) => ({ - pointer: `/lists/${listIndex}/cards/-` as Pointer, - label: `${list.name} /cards/-`, - })), - [doc.value], - ); - const clipboardSnapshot = doc.clipboard.read(); - const payloadValue = useMemo(() => { - const parsed = parseJson(payload); - if (!parsed.ok) return { __invalid_json: parsed.message }; - return parsed.value; - }, [payload]); - const pointsValue = useMemo(() => parseNumberPayload(pointsPayload), [pointsPayload]); - const badPointsValue = useMemo(() => parseNumberPayload(badPointsPayload), [badPointsPayload]); - const selectedSource = selectedPointers.length > 0 ? selectedPointers : valueTarget; - const primarySource = valueTarget; - const targetTitlePath = `${valueTarget}/title` as Pointer; - const targetPointsPath = `${valueTarget}/points` as Pointer; - const canInsertPayload = doc.canInsert(insertTarget, payloadValue); - const canPatchReplaceTitle = doc.canPatch([{ op: "replace", path: targetTitlePath, value: textPayload }]); - const canPatchReplacePoints = doc.canPatch([{ op: "replace", path: targetPointsPath, value: pointsValue }]); - const canPatchBadPoints = doc.canPatch([{ op: "replace", path: targetPointsPath, value: badPointsValue }]); - const canPatchDeleteTarget = doc.canPatch([{ op: "remove", path: valueTarget }]); - const canDuplicateTarget = doc.canDuplicate(valueTarget, { rekey: cardRekey() }); - const canMoveTarget = doc.canMove(valueTarget, insertTarget); - const canReplaceTargetTitle = doc.canReplace(targetTitlePath, textPayload); - const canDeleteSource = doc.canDelete(selectedSource); - const canCopySource = doc.canCopy(selectedSource); - const canCopyPrimary = doc.canCopy(primarySource); - const canCutSource = doc.canCut(selectedSource); - const canPasteClipboardAfterTarget = doc.canPaste({ after: valueTarget }); - const canPasteClipboardToInsertTarget = doc.canPaste(insertTarget, { spread: true, rekey: cardRekey() }); - const canInsertPayloadToInsertTarget = doc.canInsert(insertTarget, payloadValue, { rekey: cardRekey() }); - const canFindQuery = doc.canFind(query); - const canUndo = doc.canUndo(); - const canRedo = doc.canRedo(); - const selectedCount = selectedPointers.length; - const activeStage = featureStages.find((stage) => stage.id === activeStageId) ?? featureStages[0]!; - const selectedCardReason = selectedCount === 1 - ? undefined - : selectedCount === 0 - ? "state: select_one_card" - : "state: single_card_only"; - const bulkSelectionReason = selectedCount > 1 ? undefined : "state: select_multiple_cards"; - - const run = (call: string, action: () => unknown, feature?: string): void => { - const before = doc.value; - const bindings = feature ? featureBindings(feature, call, before) : []; - try { - const value = action(); - const output = value ?? doc.value; - const effects = operationEffects(output); - const next: BenchResult = { - call, - value: output, - feature, - bindings, - effect: effects.length > 0 - ? effects - : mutatesBoard(call) - ? boardEffect(before, resultBoard(output, doc.value)) - : [], - }; - setResult(next); - if (feature) { - setFeatureResults((current) => ( - call.includes("doc.load(") || call.includes("doc.reset(") - ? { [feature]: next } - : { ...current, [feature]: next } - )); - } - } catch (error) { - const next: BenchResult = { - call, - value: error instanceof Error ? error.message : error, - feature, - bindings, - effect: [], - }; - setResult(next); - if (feature) setFeatureResults((current) => ({ ...current, [feature]: next })); - } - }; - - const featureResult = (feature: string): BenchResult | undefined => ( - featureResults[feature] - ); - - const changeInsertTarget = (target: Pointer): void => { - setInsertTarget(target); - const status = statusForInsertTarget(doc.value, target); - if (status) setPayload((current) => payloadWithStatus(current, status)); - }; - - const selectNoCards = (): unknown => { - doc.selection?.empty(); - return doc.selection?.snapshot(); - }; - - const selectFirstCard = (): unknown => { - const pointer = cardPointer(0, 0); - setValueTarget(pointer); - doc.selection?.collapse(pointer); - return doc.selection?.snapshot(); - }; - - const featureBindings = (feature: string, call: string, board: Board): string[] => { - const title = cardTitleAt(board, valueTarget); - if (feature === "Insert card") { - const card = payloadValue as { title?: unknown; status?: unknown }; - return [ - `target ${insertTarget}`, - `payload ${card.title ?? "payload"}`, - `status ${card.status ?? "payload"}`, - ]; - } - if (feature === "Edit card") return [`path ${targetTitlePath}`, `value ${textPayload}`]; - if (feature === "Move card") return [`source ${valueTarget}`, `target ${insertTarget}`, title ? `card ${title}` : "card unknown"]; - if (feature === "Duplicate card") return [`source ${valueTarget}`, title ? `card ${title}` : "card unknown", "rekey id:suffix"]; - if (feature === "Find and select") return [`query ${query}`]; - if (feature === "Copy and paste") { - if (call.includes("canCopy") || call.includes("copy(")) return [`source ${primarySource}`, title ? `card ${title}` : "card unknown"]; - if (call.includes("after")) return [`target after ${valueTarget}`, title ? `after ${title}` : "after unknown"]; - return [`target ${insertTarget}`]; - } - if (feature === "Bulk cards") return [`source ${selectedLabel(Array.isArray(selectedSource) ? selectedSource : [selectedSource])}`]; - if (feature === "Selection set") return [`selected ${selectedCount}`]; - if (feature === "Undo and redo") return [`undo ${doc.history.undoDepth}`, `redo ${doc.history.redoDepth}`]; - if (feature === "Read schema") { - if (call.includes("schema.")) return [`schema target ${insertTarget}`]; - if (call.includes("entries")) return ['entries /lists/0/cards']; - return [`value ${valueTarget}`]; - } - if (feature === "Board plumbing") { - if (call.includes("doc.load(")) return ["fixture Loaded fixture"]; - if (call.includes("doc.reset(")) return ["initial board"]; - if (call.includes("doc.subscribe(")) return ["watch /settings/owner"]; - if (call.includes("applyPatch(")) return ["external patch"]; - if (call.includes("pointer helpers")) return ["path /lists/0/cards/0/title"]; - if (call.includes("trackPointer(")) return ["track /lists/0/cards/1/title"]; - if (call.includes("clipboard.write(")) return [`source ${valueTarget}`]; - if (call.includes("clipboard.clear(")) return ["clipboard buffer"]; - if (call.includes("selection?.textPatch(")) return [`text ${textPayload}`]; - if (call.includes("schema.kind(")) return [`value ${valueTarget}`]; - } - return []; - }; - - const parsedPayload = (): unknown => { - return payloadValue; - }; - - const insertCardToTodo = (): unknown => doc.insert(insertTarget, parsedPayload()); - - const copySelection = (): unknown => doc.copy(selectedSource); - - const copyPrimaryCard = (): unknown => doc.copy(primarySource); - - const pasteClipboardAfterTarget = (): unknown => doc.paste({ after: valueTarget }); - - const pasteClipboardToInsertTarget = (): unknown => doc.paste(insertTarget, { - spread: true, - rekey: cardRekey(), - }); - - const insertPayloadAfterTarget = (): unknown => doc.insert( - { after: valueTarget }, - parsedPayload(), - { rekey: cardRekey() }, - ); - - const insertPayloadToInsertTarget = (): unknown => doc.insert( - insertTarget, - parsedPayload(), - { rekey: cardRekey() }, - ); - - const duplicateTarget = (): unknown => { - const duplicated = doc.duplicate(valueTarget, { rekey: cardRekey() }); - if (duplicated.ok) { - setValueTarget(duplicated.duplicatedTo); - doc.selection?.collapse(duplicated.duplicatedTo); - } - return duplicated; - }; - - const selectTodoCards = (): unknown => { - const matches = doc.find(query); - if (!matches.ok) return matches; - doc.selection?.selectRanges(matches.pointers, undefined, undefined, Math.max(0, matches.pointers.length - 1)); - return matches; - }; - - const replaceTitleText = (): unknown => { - const selection = doc.selection?.snapshot(); - const hasTitleSelection = selection?.selectedPointers.includes("/title") ?? false; - if (!hasTitleSelection) doc.selection?.collapse({ path: "/title", offset: 0 }); - const planned = doc.selection?.textPatch(textPayload); - return planned?.ok - ? doc.commit(planned.patch, { mergeKey: "title-text", selectionAfter: planned.selection }) - : planned; - }; - - const commitReplaceTitle = (): unknown => { - return doc.commit( - [{ op: "replace", path: targetTitlePath, value: textPayload }], - { label: "commit", selectionAfter: collapsedSelection(valueTarget) }, - ); - }; - - const transactionRename = (): unknown => { - doc.history.transaction({ label: "rename-two" }, () => { - doc.patch({ op: "replace", path: "/lists/0/cards/0/title", value: "Batch A" }); - doc.patch({ op: "replace", path: "/lists/0/cards/1/title", value: "Batch B" }); - }); - return doc.value; - }; - - const queryPointers = (): unknown => doc.find(query); - - const inspectCreateJSONDocument = (): unknown => { - const headless = createJSONDocument(BoardSchema, initialBoard, { - history: 10, - selection: { mode: "extended", initial: [cardPointer(0, 0)] }, - }); - headless.patch({ op: "copy", from: cardPointer(0, 0), path: "/lists/0/cards/-" }); - headless.selection?.togglePointer(cardPointer(0, 1)); - headless.copy(headless.selection?.selectedPointers ?? []); - headless.paste("/lists/1/cards/-"); - - return { - title: headless.value.title, - cards: headless.value.lists.map((list) => list.cards.length), - selection: headless.selection?.selectedPointers, - canUndo: headless.history.canUndo, - }; - }; - - const inspectApplyOperation = (): unknown => applyOperation(BoardSchema, doc.value, { - op: "replace", - path: "/title", - value: "Single op", - }); - - const inspectApplyPatch = (): unknown => { - const patch: JSONPatchOperation[] = [ - { op: "add", path: "/lists/0/cards/0/tags/-", value: "patched" }, - { op: "move", from: "/lists/0/cards/1", path: "/lists/1/cards/1" }, - ]; - return applyPatch(BoardSchema, doc.value, patch); - }; - - const inspectApplyPatchToTrustedState = (): unknown => applyPatchToTrustedState(BoardSchema, doc.value, [ - { op: "replace", path: "/settings/owner", value: "trusted" }, - ]); - - const inspectBoundaryErrors = (): unknown => { - let pointerError = false; - try { - parsePointer("lists/0/cards/0" as Pointer); - } catch (error) { - pointerError = error instanceof PointerSyntaxError; - } - const crudError = new JSONDocumentError("patch", { - ok: false, - code: "path_not_found", - reason: "demo", - } as never); - return { - JSONDocumentError: { name: crudError.name, message: crudError.message }, - PointerSyntaxError: pointerError, - }; - }; - - const inspectPointerHelpers = (): unknown => { - return { - parsePointer: parsePointer("/lists/0/cards/0"), - tryParsePointer: tryParsePointer("/lists/0/cards/0"), - buildPointer: buildPointer(["lists", 0, "cards", 0]), - appendSegment: appendSegment("/lists/0/cards", 0), - parentPointer: parentPointer("/lists/0/cards/0/title"), - lastSegment: lastSegment("/lists/0/cards/0/title"), - lastSegmentIndex: lastSegmentIndex("/lists/0/cards/12"), - withLastSegment: withLastSegment("/lists/0/cards/0/title", "points"), - escapeSegment: escapeSegment("a/b~c"), - unescapeSegment: unescapeSegment("a~1b~0c"), - }; - }; - - const inspectTrackPointer = (): unknown => trackPointer("/lists/0/cards/1/title", [ - { op: "add", path: "/lists/0/cards/0/tags/-", value: "patched" }, - { op: "move", from: "/lists/0/cards/1", path: "/lists/1/cards/1" }, - ]); - - const inspectReactFacade = (): unknown => ({ - value: doc.value, - selection: doc.selection?.snapshot(), - history: { undo: doc.history.undoDepth, redo: doc.history.redoDepth }, - clipboard: doc.clipboard.read(), - schema: doc.schema.kind(valueTarget), - }); - - const inspectSubscribe = (): unknown => { - const events: unknown[] = []; - const unsubscribe = doc.subscribe((applied, metadata) => { - events.push({ applied, metadata }); - }); - const applied = doc.patch({ op: "replace", path: "/settings/owner", value: `sub-${doc.history.undoDepth}` }); - unsubscribe(); - return { applied, events }; - }; - - const inspectSelectionReads = (): unknown => ({ - selectedPointers: doc.selection?.selectedPointers, - selectionRanges: doc.selection?.selectionRanges, - primaryIndex: doc.selection?.primaryIndex, - rangeCount: doc.selection?.rangeCount, - selectedCount: doc.selection?.selectedCount, - hasSelection: doc.selection?.hasSelection, - isCollapsed: doc.selection?.isCollapsed, - type: doc.selection?.type, - primaryRange: doc.selection?.primaryRange, - anchorPointer: doc.selection?.anchorPointer, - focusPointer: doc.selection?.focusPointer, - selectedSource: doc.selection?.selectedSource, - primaryPointer: doc.selection?.primaryPointer, - caret: doc.selection?.caret, - caretPointer: doc.selection?.caretPointer, - context: doc.selection?.context, - anchor: doc.selection?.anchor, - focus: doc.selection?.focus, - }); - - const selectionRestoreRoundtrip = (): unknown => { - const snap = doc.selection?.snapshot(); - doc.selection?.empty(); - if (snap) doc.selection?.restore(snap); - return doc.selection?.snapshot(); - }; - - const selectionSubscribeOnce = (): unknown => { - const events: unknown[] = []; - const unsubscribe = doc.selection?.subscribe((snapshot, previous) => { - events.push({ snapshot, previous }); - }); - doc.selection?.collapse(valueTarget); - unsubscribe?.(); - return events; - }; - - const resetTargets = (): void => { - setValueTarget(cardPointer(0, 0)); - setInsertTarget("/lists/0/cards/-" as Pointer); - }; - - const loadFixture = (): unknown => { - resetTargets(); - return doc.load({ - ...initialBoard, - title: "Loaded fixture", - settings: { archived: true, owner: "fixture" }, - }); - }; - const resetBoard = (): unknown => { - resetTargets(); - return doc.reset(); - }; - const deleteTarget = (): unknown => doc.delete(valueTarget); - const deleteTargets = (): unknown => doc.delete(selectedSource); - - const executeInsertCard = (): void => { - const disabled = canDisabledReason(canInsertPayload); - if (disabled) return; - run(`doc.insert("${insertTarget}", payload)`, insertCardToTodo, "Insert card"); - }; - - const executeSelectNone = (): void => { - run("doc.selection?.empty()", selectNoCards, "Selection set"); - }; - - const executeSelectOne = (): void => { - run(`doc.selection?.collapse("${cardPointer(0, 0)}")`, selectFirstCard, "Selection set"); - }; - - const executeSelectMany = (): void => { - run("doc.selection?.selectRanges(queryPointers)", selectTodoCards, "Selection set"); - }; - - const executeSelectSearchResults = (): void => { - const disabled = canDisabledReason(canFindQuery); - if (disabled) return; - run("doc.selection?.selectRanges(queryPointers)", selectTodoCards, "Find and select"); - }; - - const executeUndo = (): void => { - const disabled = canDisabledReason(canUndo); - if (disabled) return; - run("doc.undo()", () => doc.undo(), "Undo and redo"); - }; - - const executeRedo = (): void => { - const disabled = canDisabledReason(canRedo); - if (disabled) return; - run("doc.redo()", () => doc.redo(), "Undo and redo"); - }; - - const executeRenameCard = (): void => { - const disabled = selectedCardReason ?? canDisabledReason(canPatchReplaceTitle); - if (disabled) return; - run(`doc.replace("${targetTitlePath}", textPayload)`, () => doc.replace(targetTitlePath, textPayload), "Edit card"); - }; - - const executeMoveCard = (): void => { - const disabled = selectedCardReason ?? canDisabledReason(canMoveTarget); - if (disabled) return; - run(`doc.move("${valueTarget}", "${insertTarget}")`, () => doc.move(valueTarget, insertTarget), "Move card"); - }; - - const executeDuplicateCard = (): void => { - const disabled = selectedCardReason ?? canDisabledReason(canDuplicateTarget); - if (disabled) return; - run(`doc.duplicate("${valueTarget}", { rekey })`, duplicateTarget, "Duplicate card"); - }; - - const executeCopyCommand = (): void => { - if (selectedCount > 1) { - const disabled = bulkSelectionReason ?? canDisabledReason(canCopySource); - if (disabled) return; - run("doc.copy(source)", copySelection, "Bulk cards"); - return; - } - const disabled = selectedCardReason ?? canDisabledReason(canCopyPrimary); - if (disabled) return; - run(`doc.copy("${primarySource}")`, copyPrimaryCard, "Copy and paste"); - }; - - const executeCutCommand = (): void => { - const disabled = selectedCount > 1 - ? bulkSelectionReason ?? canDisabledReason(canCutSource) - : selectedCardReason ?? canDisabledReason(canCutSource); - if (disabled) return; - run("doc.cut(source)", () => doc.cut(selectedSource), selectedCount > 1 ? "Bulk cards" : "Copy and paste"); - }; - - const executePasteCommand = (): void => { - if (selectedCount > 1) { - const disabled = bulkSelectionReason ?? canDisabledReason(canPasteClipboardToInsertTarget); - if (disabled) return; - run(`doc.paste("${insertTarget}", { spread: true, rekey })`, pasteClipboardToInsertTarget, "Bulk cards"); - return; - } - const disabled = selectedCardReason ?? canDisabledReason(canPasteClipboardAfterTarget); - if (disabled) return; - run(`doc.paste({ after: "${valueTarget}" })`, pasteClipboardAfterTarget, "Copy and paste"); - }; - - const executeDeleteCommand = (): void => { - if (selectedCount > 1) { - const disabled = bulkSelectionReason ?? canDisabledReason(canDeleteSource); - if (disabled) return; - run("doc.delete(source)", deleteTargets, "Bulk cards"); - return; - } - const disabled = selectedCardReason ?? canDisabledReason(canPatchDeleteTarget); - if (disabled) return; - run(`doc.delete("${valueTarget}")`, deleteTarget, "Delete card"); - }; - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent): void => { - if (event.defaultPrevented || event.repeat || isEditableShortcutTarget(event.target)) return; - - const key = event.key.toLowerCase(); - const mod = event.metaKey || event.ctrlKey; - if (mod && !event.altKey && key === "z") { - event.preventDefault(); - if (event.shiftKey) executeRedo(); - else executeUndo(); - return; - } - if (mod && !event.altKey && key === "y") { - event.preventDefault(); - executeRedo(); - return; - } - if (mod || event.altKey) return; - - if (key === "n") { - event.preventDefault(); - executeInsertCard(); - } else if (key === "f") { - event.preventDefault(); - executeSelectSearchResults(); - } else if (key === "e") { - event.preventDefault(); - executeRenameCard(); - } else if (key === "m") { - event.preventDefault(); - executeMoveCard(); - } else if (key === "d") { - event.preventDefault(); - executeDuplicateCard(); - } else if (key === "c") { - event.preventDefault(); - executeCopyCommand(); - } else if (key === "x") { - event.preventDefault(); - executeCutCommand(); - } else if (key === "v") { - event.preventDefault(); - executePasteCommand(); - } else if (key === "delete" || key === "backspace") { - event.preventDefault(); - executeDeleteCommand(); - } else if (key === "0") { - event.preventDefault(); - executeSelectNone(); - } else if (key === "1") { - event.preventDefault(); - executeSelectOne(); - } else if (key === "2") { - event.preventDefault(); - executeSelectMany(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }); - - const valueTargetInput = (label: string): ReactNode => ( - - - - ); - - const insertTargetInput = (label: string): ReactNode => ( - - - - ); - - const payloadInput = (label: string): ReactNode => ( - -