Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ All notable changes to this project are documented here.
immutable canonical patch records. Mutable ingress values are owned before
commit; pre-frozen `trustedInitial` snapshots retain the identity-preserving
zero-copy path, while only statically known-JSON schemas skip the JSON scan.
- Changed overlapping replacement validation to inspect the effective final
targets after one atomic batch apply. A later descendant or ancestor replace
may repair an intermediate invalid value; legacy failure ordering remains the
fallback when the batch itself cannot be applied.

### Fixed

Expand All @@ -51,6 +55,8 @@ All notable changes to this project are documented here.
live state, failed previews, or capability probes.
- Kept publication-local patch records and revision order across reentrant
subscribers, and stopped test-only/no-op patches from creating stale history.
- Rejected inherited prototype properties during schema pointer introspection
and custom standalone string checks used as record-key schemas.

### Performance

Expand Down Expand Up @@ -80,6 +86,9 @@ All notable changes to this project are documented here.
instead of rescanning a validated large base for every replay step.
- Added `perf:causal` to compare revision-suffix materialization with the legacy
full-journal path at configurable journal sizes.
- Consolidated root-object, same-array, independent, and overlapping replace
schema validation behind one final-target planner and the foundation patch
executor, removing the duplicate schema-side replace executors.

## 1.1.0-rc.0 - 2026-07-11

Expand Down
8 changes: 5 additions & 3 deletions docs/standard/foundation-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,11 @@ Remote transport와 conflict policy는 계속 core 밖에 둔다.
성능상 prepare 단계의 private draft는 같은 batch에서 겹치는 ancestor/descendant
replace가 있어도 touched container를 commit당 한 번만 복제한다. 최적화 경로는
순차 RFC 6902 결과, 실패 atomicity, applied order, structural sharing을 reference
경로와 동일하게 유지해야 한다. Plain structural schema의 overlapping replace는 각
value가 target schema에 허용됨을 빠르게 증명할 수 있을 때만 이 bulk 경로로 승격하고,
그 외 배치는 기존 validation/error-order 경로로 되돌린다.
경로와 동일하게 유지해야 한다. Plain structural schema의 overlapping replace는 batch를
한 번 적용한 뒤 실제로 남은 final target만 검증한다. 따라서 중간의 invalid value가 뒤의
ancestor/descendant replace로 복구되면 성공할 수 있다. Batch 적용 실패, 지원하지 않는
schema 경로, 기존 진단 순서를 보존해야 하는 형태는 canonical full validation 또는 기존
sequential error-order 경로로 되돌린다.

History inverse 계산도 같은 private sequential-replace COW seam을 재사용한다. 각 write
직전 값을 캡처해 기존 operation별 inverse와 undo 순서를 유지하며, inverse를 상위 path
Expand Down
72 changes: 0 additions & 72 deletions packages/json-document/src/domain/schema/array/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
type Pointer,
} from "../../../foundation/pointer/index.js";
import type { JSONPatchOperation } from "../../../foundation/patch/index.js";
import { validateOperationShape } from "../../../foundation/patch/index.js";
import { numericSegment } from "../../../foundation/patch/index.js";

export type AppliedLocalOpSourceValue = { ok: true; value: unknown } | { ok: false };
Expand Down Expand Up @@ -53,67 +52,6 @@ export function arrayIndexPathLocation(
return index === null ? null : { parent, parentSegments: segments.slice(0, -1), index };
}

export function arrayElementReplaceLocation(
path: Pointer,
): { parent: Pointer; parentSegments: string[]; index: number } | null {
const simple = parseSimpleArrayElementReplacePath(path);
if (simple !== null) return simple;

const parent = parentPointer(path);
if (parent === null) return null;
let segments: string[];
try {
segments = parsePointer(path);
} catch {
return null;
}
const segment = segments[segments.length - 1];
if (segment === undefined) return null;
const index = numericSegment(segment);
return index === null ? null : { parent, parentSegments: segments.slice(0, -1), index };
}

export function arrayElementIndexPrefix(parent: Pointer): string {
return parent === "" ? "/" : `${parent}/`;
}

export function parseKnownArrayElementReplaceIndex(path: Pointer, prefix: string): number | null {
if (!path.startsWith(prefix)) return null;
const indexText = path.slice(prefix.length);
return indexText.includes("/") ? null : numericSegment(indexText);
}

export function planIndependentReplacePaths(operations: ReadonlyArray<JSONPatchOperation>): Pointer[] | null {
if (!Array.isArray(operations) || operations.length === 0) return null;
const paths = new Array<Pointer>(operations.length);

for (let index = 0; index < operations.length; index++) {
if (!(index in operations)) return null;
const op = operations[index]!;
if (validateOperationShape(op) !== null || op.op !== "replace" || typeof op.path !== "string" || op.path === "") return null;
try {
const segments = parsePointer(op.path);
if (segments.includes("-")) return null;
} catch {
return null;
}
paths[index] = op.path;
}

return paths;
}

export function haveIndependentReplacePaths(paths: ReadonlyArray<Pointer>): boolean {
if (!Array.isArray(paths) || paths.length === 0) return false;
const sorted = [...paths].sort();
for (let index = 1; index < sorted.length; index++) {
const previous = sorted[index - 1]!;
const current = sorted[index]!;
if (current === previous || current.startsWith(`${previous}/`)) return false;
}
return true;
}

export function readArrayAtSegments(
state: unknown,
segments: ReadonlyArray<string>,
Expand All @@ -139,13 +77,3 @@ function parseSimpleArrayIndexPath(path: Pointer): { parent: Pointer; index: num
const index = segment === "-" ? "-" : numericSegment(segment);
return index === null ? null : { parent: path.slice(0, indexSlash), index };
}

function parseSimpleArrayElementReplacePath(path: Pointer): { parent: Pointer; parentSegments: string[]; index: number } | null {
if (path === "" || path[0] !== "/" || path.includes("~")) return null;
const indexSlash = path.lastIndexOf("/");
if (indexSlash < 0) return null;
const index = numericSegment(path.slice(indexSlash + 1));
if (index === null) return null;
const parent = path.slice(0, indexSlash);
return { parent, parentSegments: parent === "" ? [] : parent.slice(1).split("/"), index };
}
Loading