From 5e3938b2808b491fad6ce7cfe44315a6688083b7 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 01:02:26 -0700 Subject: [PATCH 01/16] Document graph-backed rope runtime discovery --- ...149-graph-backed-rope-runtime-discovery.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 docs/design/0149-graph-backed-rope-runtime-discovery.md diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md new file mode 100644 index 00000000..e80d3322 --- /dev/null +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -0,0 +1,172 @@ +# Graph-Backed Rope Runtime Discovery + +Status: discovery summary and design gate + +Date: 2026-07-04 + +## Summary + +An external audit called out a real architectural drift in jedit's current text +runtime. The blunt version is correct: the code currently named around +`RopeHead`, `BufferRoot`, `replaceRangeAsTick`, and `HotTextBufferState` does not +implement a graph-backed rope runtime. + +The current hot text runtime still uses full text snapshots: + +- [`src/domain/text-edit-contract.ts`](../../src/domain/text-edit-contract.ts) + encodes the entire buffer into UTF-8 bytes, splices the requested range, + decodes a new full string, and wraps that string in a new `BufferRoot`. +- [`src/ports/hot-text-runtime.ts`](../../src/ports/hot-text-runtime.ts) + defines `HotTextBufferState.roots` as an array of retained roots. +- [`src/adapters/in-memory-hot-text-runtime.ts`](../../src/adapters/in-memory-hot-text-runtime.ts) + appends the new full root to that retained array after an admitted edit. +- [`src/adapters/installed-jedit-contract-echo-transport.ts`](../../src/adapters/installed-jedit-contract-echo-transport.ts) + still defaults the installed jedit contract transport to that in-memory + runtime. + +That means the current implementation has O(N) edit cost and O(N) retained text +per edit for buffer size N. For repeated small edits on a large file, retained +memory grows with file size times edit count. That is not the intended jedit +architecture. + +## What Was Intended + +The original design intent is stronger than "a text runtime behind an Echo-shaped +API." The rope should be modeled with Echo graph primitives through the jedit +contract layer. Echo intents should evolve that graph by rewriting rope facts and +producing a new rope model in the Echo-hosted causal history. + +In that model: + +- `BufferWorldline` is the logical editable object. +- `RopeHead` identifies one graph-backed text state. +- `RopeBranch`, `RopeLeaf`, and `TextBlob` are graph facts, not names for a full + JavaScript string. +- `replaceRangeAsTick` is an Echo intent that reads a base head, range-closes + over touched rope nodes, creates the new local rope facts, emits rewrite and + diff evidence, and advances the worldline head. +- `RopeRewrite`, `RopeDiff`, tick receipts, checkpoints, anchors, strands, and + admissions are retained causal evidence. +- Materialized strings are readings or projections. They are allowed for UI + rendering, export, save, tests, and caches, but they are not the source of + editor truth. + +Echo still remains generic. jedit owns the text and rope contract vocabulary; +Echo hosts generic admission, scheduling, receipts, retention, and causal +storage. The important correction is that the jedit contract vocabulary must be +real graph-backed state, not labels over full string snapshots. + +## Why This Matters + +This is not a minor performance issue. + +The current runtime breaks the product's intended causal posture in several +ways: + +- It makes the authoritative retained state a list of full materialized strings + rather than compact causal graph evidence. +- It makes the rope vocabulary misleading because no rope graph is actually + being evolved. +- It makes `:why`, historical basis preview, strands, braids, gutter evidence, + and retention policy harder to make honest because they must recover history + from full roots instead of reading graph facts. +- It can make dogfooding painful on larger files because every edit copies and + retains the whole buffer. +- It risks making UI language more causally honest than the storage model below + it. + +The existing docs already point in the right direction. In particular: + +- [`0003-echo-backed-rope-worldline-contract.md`](0003-echo-backed-rope-worldline-contract/echo-backed-rope-worldline-contract.md) + says witnessed causal history is canonical and materialized projections are + not editor truth. +- [`jedit-echo-graph-model.md`](jedit-echo-graph-model.md) describes the desired + `BufferWorldline -> RopeHead -> Rope DAG` shape and states that + `ReplaceRangeAsTick` should reuse untouched subtrees. +- [`0027-echo-hosted-production-cutover.md`](0027-echo-hosted-production-cutover.md) + says the local in-memory text model is no longer a production authority target. +- [`structural-history-graphql-authority.md`](structural-history-graphql-authority.md) + says the TypeScript model is transitional evidence, not durable authority. + +The problem is that implementation reality has not caught up to those design +claims. + +## Planned Response + +We should not patch this by making full-string replacement faster. That would +preserve the wrong architecture. The response needs to be a graph-backed runtime +cutover. + +### 1. Fence The Fixture + +The current in-memory full-snapshot runtime must be renamed, documented, or +otherwise fenced as a fixture or transitional adapter. It should not silently be +the default production text authority for daily-driver jedit. + +Planned guardrails: + +- rename or document the runtime as full-snapshot/transitional; +- add a production guard against implicit default use where possible; +- keep focused tests able to inject it deliberately; +- make release and preflight checks fail if product code starts treating it as + durable text authority again. + +### 2. Write The Runtime Design + +Create a full graph-backed rope runtime design that specifies: + +- graph facts and relations for worldlines, heads, branches, leaves, blobs, + rewrites, diffs, ticks, checkpoints, anchors, strands, and admissions; +- intent semantics for `createBufferWorldline`, `replaceRangeAsTick`, and + `createCheckpoint`; +- reading semantics for `textWindow`, `worldlineSnapshot`, save/export, source + highlighting, Graft, and `:why`; +- retention and compaction policy for graph facts, text blobs, receipts, + checkpoints, and materialized projections; +- cutover strategy from the current full-root fixture to the graph-backed + implementation. + +### 3. Add Witnesses That Fail The Current Architecture + +Before implementation, add explicit witnesses for the non-negotiable properties: + +- repeated small edits to a large buffer must not retain one full text snapshot + per edit as authoritative history; +- `replaceRangeAsTick` must preserve identity for untouched subtrees; +- no-op replacement must not mint a tick; +- text-window reads must materialize from a rope head, not from a retained full + root list; +- save/export must read from a causal basis and must not mutate text authority; +- `:why` for a byte range must be able to cite graph-backed rewrite/diff + evidence. + +### 4. Implement The Cutover In Slices + +The likely implementation sequence is: + +1. graph-backed `createBufferWorldline`; +2. graph-backed `textWindow` over a `RopeHead`; +3. graph-backed single-range `replaceRangeAsTick`; +4. graph-backed `createCheckpoint`; +5. production session cutover to the graph-backed runtime; +6. quarantine or delete full-root production authority paths; +7. update `:why`, gutter evidence, worldline drawers, and save/export posture to + consume graph facts directly; +8. define compaction and cold-retention rules. + +### 5. Treat UI Work As Dependent On Runtime Honesty + +UI posture work, including the causal footer and gutter evidence work, should be +checked against the runtime truth. If the UI says "basis", "head", "tick", +"checkpoint", or "worldline", the source underneath should be graph-backed +causal evidence or explicitly marked as a transitional projection. + +## Immediate Decision + +Do not treat the current full-snapshot hot text runtime as an acceptable +production implementation. It can remain only as a bounded fixture while the +graph-backed runtime is designed and cut over. + +The next work item is a careful design document for the graph-backed rope +runtime, followed by failing witnesses that make the current architecture's +retention and rewrite behavior unacceptable for production. From 004733e7414672fe91081d7a6a0f9c15ca1aa6ef Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 01:15:56 -0700 Subject: [PATCH 02/16] Enhance graph-backed rope runtime gate --- ...149-graph-backed-rope-runtime-discovery.md | 479 ++++++++++++++++-- 1 file changed, 436 insertions(+), 43 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index e80d3322..d3a67020 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -97,64 +97,444 @@ We should not patch this by making full-string replacement faster. That would preserve the wrong architecture. The response needs to be a graph-backed runtime cutover. -### 1. Fence The Fixture +The hard gate is: -The current in-memory full-snapshot runtime must be renamed, documented, or -otherwise fenced as a fixture or transitional adapter. It should not silently be -the default production text authority for daily-driver jedit. +```text +Do not begin more UI causal-honesty work until the text runtime has at least one +real graph-backed path for create, read, replace, and checkpoint. +``` + +Until then, UI labels such as `basis`, `head`, `tick`, `checkpoint`, and +`worldline` must either be backed by graph facts or explicitly marked as +transitional projection posture. + +### 1. Rename And Fence The Fixture + +The current in-memory full-snapshot runtime must be renamed so accidental +production use is visibly wrong. Acceptable names include: + +- `FullSnapshotHotTextRuntimeFixture` +- `InMemoryFullSnapshotTextRuntime` +- `TransitionalSnapshotTextRuntime` + +Unacceptable names include: + +- `InMemoryHotTextRuntime` +- `HotTextRuntime` +- `DefaultHotTextRuntime` +- `ProductionHotTextRuntime` + +The name should make the wrong wiring ugly. Planned guardrails: - rename or document the runtime as full-snapshot/transitional; -- add a production guard against implicit default use where possible; +- add a production guard against implicit default use; - keep focused tests able to inject it deliberately; - make release and preflight checks fail if product code starts treating it as durable text authority again. -### 2. Write The Runtime Design +The installed/default transport must not silently instantiate the full-snapshot +runtime. If a temporary escape hatch is required, it should be explicit: + +```typescript +if (process.env.JEDIT_ALLOW_FULL_SNAPSHOT_TEXT_AUTHORITY !== "1") { + throw new Error( + "FullSnapshotHotTextRuntimeFixture cannot be used as production text authority.", + ); +} +``` + +Tests may opt in deliberately. Product startup should not. + +### 2. Define Coordinates Before Facts + +The graph-backed rope design must define its coordinate system before it defines +facts. The current code uses UTF-8 byte ranges, JavaScript strings are UTF-16, +and editor UI needs line/column positions. Those must not blur together. + +Authoritative mutation coordinates should be UTF-8 byte offsets. UI coordinates +should be adapters over that storage coordinate. + +The design should introduce branded coordinate types: + +```typescript +type ByteOffset = number & { readonly __brand: "utf8-byte-offset" }; +type Utf16Offset = number & { readonly __brand: "utf16-code-unit-offset" }; + +interface LineColumn { + readonly line: number; + readonly columnUtf16: Utf16Offset; +} +``` + +Rules: + +- rope mutation ranges are half-open UTF-8 byte ranges; +- text blobs store UTF-8 bytes; +- line/column and UTF-16 offsets are UI or protocol projections; +- grapheme-aware movement is a command-planning concern over readings, not the + authoritative storage coordinate; +- every conversion must cite the basis head or reading it was computed from. + +### 3. Define Real Typed Graph Facts + +Create a full graph-backed rope runtime design that specifies concrete fact +shapes. The exact names may evolve, but the design must be precise enough for +witnesses to target. + +Example fact skeleton: + +```typescript +type WorldlineId = string & { readonly __brand: "WorldlineId" }; +type RopeHeadId = string & { readonly __brand: "RopeHeadId" }; +type RopeNodeId = string & { readonly __brand: "RopeNodeId" }; +type TextBlobId = string & { readonly __brand: "TextBlobId" }; +type TickId = string & { readonly __brand: "TickId" }; +type Hash = string & { readonly __brand: "Hash" }; + +interface BufferWorldlineFact { + readonly kind: "jedit.text.BufferWorldline"; + readonly worldlineId: WorldlineId; + readonly createdAtTick: TickId; + readonly initialHeadId: RopeHeadId; +} + +interface RopeHeadFact { + readonly kind: "jedit.text.RopeHead"; + readonly headId: RopeHeadId; + readonly worldlineId: WorldlineId; + readonly rootNodeId: RopeNodeId; + readonly basisHeadId?: RopeHeadId; + readonly createdByTickId: TickId; + readonly byteLength: number; + readonly lineCount: number; + readonly contentHash: Hash; +} + +interface RopeBranchFact { + readonly kind: "jedit.text.RopeBranch"; + readonly nodeId: RopeNodeId; + readonly left: RopeNodeId; + readonly right: RopeNodeId; + readonly byteLength: number; + readonly lineCount: number; + readonly height: number; + readonly contentHash: Hash; +} + +interface RopeLeafFact { + readonly kind: "jedit.text.RopeLeaf"; + readonly nodeId: RopeNodeId; + readonly blobId: TextBlobId; + readonly byteStart: ByteOffset; + readonly byteLength: number; + readonly lineCount: number; + readonly contentHash: Hash; +} + +interface TextBlobFact { + readonly kind: "jedit.text.TextBlob"; + readonly blobId: TextBlobId; + readonly encoding: "utf8"; + readonly byteLength: number; + readonly contentHash: Hash; +} +``` + +The full design must also define facts for: + +- `RopeRewrite`; +- `RopeDiff`; +- `TickReceipt`; +- `RopeCheckpoint`; +- anchors; +- strands, braids, and admissions when their implementation slice begins. + +Echo remains generic. jedit owns these fact shapes and text-specific witnesses. + +### 4. Separate Text Authority From Observations + +No-op behavior needs causal precision. A no-op replacement should not mint a new +text head or rewrite evidence claiming text changed. The system may still record +an admitted no-op intent, rejected edit, idempotent command, observation, or +receipt. + +The design should distinguish: + +- `ReplaceRangeIntent`; +- `ReplaceRangeAdmission`; +- `RopeRewrite | null`; +- `RopeDiff | null`; +- `WorldlineAdvance | null`; +- `TickReceipt`. + +Rules: + +- no text change means no new `RopeHead`; +- no changed range means no `RopeRewrite`; +- optional admission or receipt evidence may still exist; +- no-op evidence must not pollute the rope graph as if bytes changed. + +### 5. Make Untouched Subtree Identity A Contract + +Untouched subtree identity is the central rope property. If a narrow replacement +rebuilds the whole tree, it is not the intended runtime. + +Witness shape: + +```typescript +const before = await runtime.debugRopeShape(headA); +const result = await runtime.replaceRangeAsTick({ + worldlineId, + basisHeadId: headA, + range, + replacement, +}); +const after = await runtime.debugRopeShape(result.nextHeadId); + +expect(after.untouchedLeftSubtreeId).toEqual(before.untouchedLeftSubtreeId); +expect(after.untouchedRightSubtreeId).toEqual(before.untouchedRightSubtreeId); +``` + +This should be part of the contract, not an incidental optimization. + +### 6. Make Retention Measurable + +Do not rely on qualitative claims. Add an explicit witness around retained +authoritative bytes. + +Example target: + +```text +largeBufferSize = 10_000_000 bytes +edits = 1_000 single-byte edits +``` + +The full-snapshot runtime retains roughly 10 GB of authoritative text snapshots. +The graph-backed runtime should retain approximately: + +- initial text blobs; +- changed leaves; +- path-copied branch nodes; +- rewrite and diff facts; +- receipts; +- indexes and checkpoints. + +The exact byte count may vary, but the witness must assert retained +authoritative text is not O(buffer_size * edit_count). + +### 7. Define Materialization Boundaries + +Materialized strings are allowed only as readings or projections. Every +materialized string must answer: -Create a full graph-backed rope runtime design that specifies: +- which `RopeHead` was read; +- which UTF-8 byte range was read; +- whether the materialization came from cache; +- how the cache was validated against the head. -- graph facts and relations for worldlines, heads, branches, leaves, blobs, - rewrites, diffs, ticks, checkpoints, anchors, strands, and admissions; -- intent semantics for `createBufferWorldline`, `replaceRangeAsTick`, and - `createCheckpoint`; -- reading semantics for `textWindow`, `worldlineSnapshot`, save/export, source - highlighting, Graft, and `:why`; -- retention and compaction policy for graph facts, text blobs, receipts, - checkpoints, and materialized projections; -- cutover strategy from the current full-root fixture to the graph-backed - implementation. +Good: -### 3. Add Witnesses That Fail The Current Architecture +```typescript +const text = await runtime.textWindow({ + basisHeadId, + byteRange, +}); +``` -Before implementation, add explicit witnesses for the non-negotiable properties: +Bad: -- repeated small edits to a large buffer must not retain one full text snapshot - per edit as authoritative history; -- `replaceRangeAsTick` must preserve identity for untouched subtrees; -- no-op replacement must not mint a tick; -- text-window reads must materialize from a rope head, not from a retained full - root list; -- save/export must read from a causal basis and must not mutate text authority; -- `:why` for a byte range must be able to cite graph-backed rewrite/diff - evidence. +```typescript +const text = state.roots[state.roots.length - 1].text; +``` -### 4. Implement The Cutover In Slices +This boundary is what makes `:why`, historical preview, save/export, and UI +evidence trustworthy. -The likely implementation sequence is: +### 8. Write Witnesses Before Most Implementation -1. graph-backed `createBufferWorldline`; -2. graph-backed `textWindow` over a `RopeHead`; -3. graph-backed single-range `replaceRangeAsTick`; -4. graph-backed `createCheckpoint`; -5. production session cutover to the graph-backed runtime; -6. quarantine or delete full-root production authority paths; -7. update `:why`, gutter evidence, worldline drawers, and save/export posture to - consume graph facts directly; -8. define compaction and cold-retention rules. +The better implementation order is: -### 5. Treat UI Work As Dependent On Runtime Honesty +1. minimal design skeleton; +2. failing witnesses; +3. tiny graph-backed runtime; +4. refined design; +5. more witnesses; +6. production cutover. + +The witnesses are architectural teeth, not after-the-fact documentation. + +Required first witnesses: + +- snapshot fixture cannot be constructed as default production authority; +- repeated edits do not retain one full text snapshot per edit; +- untouched subtree identity survives a narrow replacement; +- no-op intent can produce admission evidence without a new head or rewrite; +- text-window reads cite a basis head and byte range; +- save/export reads from a causal basis without mutating text authority; +- `:why` can cite rewrite, diff, tick, head, leaf, and blob evidence for a byte + range. + +### 9. Build The Smallest Real Runtime First + +Do not start by solving compaction, braids, collaborative merge, source +highlighting, and `:why` all at once. The first implementation win should be: + +```text +create buffer +-> read window +-> replace small range +-> read window +-> prove untouched identity survived +-> checkpoint +``` + +Initial scope: + +- immutable rope nodes; +- content-addressed blobs; +- binary branch tree; +- append-only graph fact store; +- single-range replacement; +- text-window read; +- checkpoint fact; +- debug-only shape inspection. + +### 10. Mark Evidence Versus Indexes + +The design must distinguish durable semantic facts from rebuildable acceleration +indexes. + +Durable truth: + +- `RopeHead`; +- `RopeBranch`; +- `RopeLeaf`; +- `TextBlob`; +- `RopeRewrite`; +- `RopeDiff`; +- `TickReceipt`; +- `RopeCheckpoint`. + +Rebuildable indexes and caches: + +- line offset index; +- syntax highlighting cache; +- materialized window cache; +- source map cache; +- render layout cache; +- search index. + +Rule: + +```text +If deleting it changes history, it is evidence. +If deleting it only makes reads slower, it is an index. +``` + +### 11. Define Balance And Checkpoint Policy + +A rope that path-copies forever without balance policy eventually becomes a +linked list with better names. The design must define: + +- target leaf size; +- maximum and minimum leaf size; +- branch weight rules; +- balance invariant; +- when replacement triggers rebalance; +- whether rebalance creates causal facts; +- whether rebalance is visible to `:why`. + +Recommended posture: + +- edits create semantic rewrite evidence; +- rebalancing creates structural maintenance evidence; +- both can be retained; +- normal UI hides structural maintenance unless debugging. + +Checkpoint semantics also need precision. A checkpoint is not new text truth. It +is a durable named basis for efficient future reads, retention, or export. + +```typescript +interface RopeCheckpointFact { + readonly kind: "jedit.text.RopeCheckpoint"; + readonly checkpointId: string; + readonly worldlineId: WorldlineId; + readonly headId: RopeHeadId; + readonly createdByTickId: TickId; + readonly reason: + | "manual-save" + | "autosave" + | "retention-boundary" + | "import" + | "test-fixture"; +} +``` + +Save/export should read from a head or checkpoint. It should not mutate text +authority unless the product explicitly records a checkpoint. + +### 12. Make `:why` An Acceptance Target + +Do not let `:why` become a bolt-on archaeology tool. For a byte range, the +runtime should be able to answer: + +- this range is present in head H; +- it descends from leaf L and blob B; +- it was introduced or last touched by rewrite R; +- rewrite R was admitted by tick T; +- tick T had basis head H0; +- here is the diff evidence; +- here are related checkpoints. + +This is the runtime acceptance demo. + +### 13. Implement The Cutover In Slices + +The likely implementation sequence is: + +1. rename the snapshot runtime and add the production guard; +2. add the fixture quarantine witness; +3. define coordinate and fact skeletons; +4. add retention, subtree identity, materialization, no-op, and `:why` witnesses; +5. implement graph-backed `createBufferWorldline`; +6. implement graph-backed `textWindow` over a `RopeHead`; +7. implement graph-backed single-range `replaceRangeAsTick`; +8. implement graph-backed `createCheckpoint`; +9. cut the production session over to the graph-backed runtime; +10. quarantine or delete full-root production authority paths; +11. update `:why`, gutter evidence, worldline drawers, and save/export posture to + consume graph facts directly; +12. define compaction and cold-retention rules. + +### 14. Useful Later Ideas + +These are not first-slice requirements, but they should stay visible: + +- content-addressed `TextBlob` storage where `blobId = hash(encoding + bytes)`; +- internal-only `debugRopeShape` for witnesses and developer tools; +- transitional `textAuthorityKind` such as `full-snapshot-fixture` or + `graph-backed-rope`; +- explicit import from old snapshot roots into one graph-backed import + checkpoint; +- queryable retention policy that explains why evidence, blobs, projections, or + indexes were retained or compacted; +- a rope fact inspector drawer; +- a causal heatmap over edit ancestry; +- a retention budget dashboard; +- basis-pinned save/export receipts. + +### 15. Things Not To Do + +- Do not optimize the full-snapshot runtime as a substitute for graph-backed + authority. +- Do not let names outrun facts. A `RopeHead` must point to an actual rope. +- Do not make UI truthier than storage truth. +- Do not mix cache invalidation with authority mutation. +- Do not make compaction destroy explainability by accident. + +### 16. Treat UI Work As Dependent On Runtime Honesty UI posture work, including the causal footer and gutter evidence work, should be checked against the runtime truth. If the UI says "basis", "head", "tick", @@ -167,6 +547,19 @@ Do not treat the current full-snapshot hot text runtime as an acceptable production implementation. It can remain only as a bounded fixture while the graph-backed runtime is designed and cut over. -The next work item is a careful design document for the graph-backed rope -runtime, followed by failing witnesses that make the current architecture's -retention and rewrite behavior unacceptable for production. +The next work item is not implementation of the final runtime. It is: + +1. rename and fence the snapshot fixture; +2. add the production guard; +3. define concrete coordinate and graph fact shapes; +4. write failing witnesses for retention, subtree identity, no-op admission, + materialization basis, save/export basis, and `:why` evidence; +5. implement the smallest real graph-backed create/read/replace/checkpoint path. + +The core principle is: + +```text +A rope runtime is defined by what survives an edit. +Materialization is a reading, not reality. +Causal honesty is an end-to-end property. +``` From a2171b98e6691873ab04efe521b1a83bfd0694be Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 09:14:18 -0700 Subject: [PATCH 03/16] Fix graph-backed runtime design review findings --- ...149-graph-backed-rope-runtime-discovery.md | 93 ++++++++++++++++--- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index d3a67020..480b82b2 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -11,7 +11,7 @@ runtime. The blunt version is correct: the code currently named around `RopeHead`, `BufferRoot`, `replaceRangeAsTick`, and `HotTextBufferState` does not implement a graph-backed rope runtime. -The current hot text runtime still uses full text snapshots: +The current hot text runtime still uses full-text snapshots: - [`src/domain/text-edit-contract.ts`](../../src/domain/text-edit-contract.ts) encodes the entire buffer into UTF-8 bytes, splices the requested range, @@ -163,7 +163,7 @@ type ByteOffset = number & { readonly __brand: "utf8-byte-offset" }; type Utf16Offset = number & { readonly __brand: "utf16-code-unit-offset" }; interface LineColumn { - readonly line: number; + readonly line: number & { readonly __brand: "zero-based-line-index" }; readonly columnUtf16: Utf16Offset; } ``` @@ -172,7 +172,14 @@ Rules: - rope mutation ranges are half-open UTF-8 byte ranges; - text blobs store UTF-8 bytes; -- line/column and UTF-16 offsets are UI or protocol projections; +- `LineColumn.line` is a zero-based logical line index; +- `LineColumn.columnUtf16` is a zero-based UTF-16 code-unit offset from the start + of that logical line; +- line/column and UTF-16 offsets are UI or protocol projections, not storage + authority; +- line projection treats CRLF as one logical line break and treats bare CR and + bare LF as one logical line break each; +- newline projection never mutates stored blob bytes or save/export bytes; - grapheme-aware movement is a command-planning concern over readings, not the authoritative storage coordinate; - every conversion must cite the basis head or reading it was computed from. @@ -195,6 +202,7 @@ type Hash = string & { readonly __brand: "Hash" }; interface BufferWorldlineFact { readonly kind: "jedit.text.BufferWorldline"; + readonly schemaVersion: 1; readonly worldlineId: WorldlineId; readonly createdAtTick: TickId; readonly initialHeadId: RopeHeadId; @@ -202,6 +210,7 @@ interface BufferWorldlineFact { interface RopeHeadFact { readonly kind: "jedit.text.RopeHead"; + readonly schemaVersion: 1; readonly headId: RopeHeadId; readonly worldlineId: WorldlineId; readonly rootNodeId: RopeNodeId; @@ -214,6 +223,7 @@ interface RopeHeadFact { interface RopeBranchFact { readonly kind: "jedit.text.RopeBranch"; + readonly schemaVersion: 1; readonly nodeId: RopeNodeId; readonly left: RopeNodeId; readonly right: RopeNodeId; @@ -225,6 +235,7 @@ interface RopeBranchFact { interface RopeLeafFact { readonly kind: "jedit.text.RopeLeaf"; + readonly schemaVersion: 1; readonly nodeId: RopeNodeId; readonly blobId: TextBlobId; readonly byteStart: ByteOffset; @@ -235,6 +246,7 @@ interface RopeLeafFact { interface TextBlobFact { readonly kind: "jedit.text.TextBlob"; + readonly schemaVersion: 1; readonly blobId: TextBlobId; readonly encoding: "utf8"; readonly byteLength: number; @@ -253,6 +265,50 @@ The full design must also define facts for: Echo remains generic. jedit owns these fact shapes and text-specific witnesses. +Runtime construction and validation are part of the contract. The branded types +above are compile-time helpers only; decoded runtime payloads must pass through +jedit-owned constructors or validators before becoming facts. + +Required constructor and validator path: + +```typescript +type FactValidationErrorCode = + | "invalid-kind" + | "invalid-schema-version" + | "invalid-id" + | "invalid-reference" + | "invalid-metric" + | "invalid-hash" + | "hash-mismatch"; + +type FactValidationResult = + | { readonly ok: true; readonly fact: TFact } + | { readonly ok: false; readonly code: FactValidationErrorCode }; + +declare function makeTextBlobFact(bytes: Uint8Array): TextBlobFact; +declare function validateRopeFact(payload: object): FactValidationResult< + | BufferWorldlineFact + | RopeHeadFact + | RopeBranchFact + | RopeLeafFact + | TextBlobFact +>; +``` + +Validation rules: + +- `kind` and `schemaVersion` are mandatory runtime tags; +- IDs must be non-empty canonical IDs in the expected namespace; +- numeric metrics must be non-negative integers; +- branch children, head roots, and leaf blobs must reference facts available in + the same write set or an already admitted basis; +- `TextBlobFact.blobId` and `contentHash` must be derived from + `encoding + bytes`, not trusted from caller input; +- branch, leaf, and head hashes must be recomputed from child/blob references and + metrics before admission; +- invalid facts are rejected before Echo admission and never become retained + authority. + ### 4. Separate Text Authority From Observations No-op behavior needs causal precision. A no-op replacement should not mint a new @@ -293,12 +349,27 @@ const result = await runtime.replaceRangeAsTick({ }); const after = await runtime.debugRopeShape(result.nextHeadId); -expect(after.untouchedLeftSubtreeId).toEqual(before.untouchedLeftSubtreeId); -expect(after.untouchedRightSubtreeId).toEqual(before.untouchedRightSubtreeId); +const preserved = compareUntouchedStructure({ + before, + after, + changedRange: range, +}); + +expect(preserved.rebuiltUntouchedSpans).toEqual([]); +expect(preserved.preservedSubtreeIds).toEqual( + preserved.expectedUntouchedSubtreeIds, +); ``` This should be part of the contract, not an incidental optimization. +The witness must be recursive. `debugRopeShape` should expose each node's byte +span within the head, node ID, child IDs, hash, and structural-maintenance +evidence if a rebalance touched otherwise unchanged text. The comparison should +walk the before and after shapes, classify spans outside the edited range as +untouched, and require every unaffected subtree identity to survive unless a +retained structural-maintenance fact explicitly explains the replacement. + ### 6. Make Retention Measurable Do not rely on qualitative claims. Add an explicit witness around retained @@ -368,7 +439,7 @@ The witnesses are architectural teeth, not after-the-fact documentation. Required first witnesses: - snapshot fixture cannot be constructed as default production authority; -- repeated edits do not retain one full text snapshot per edit; +- repeated edits do not retain one full-text snapshot per edit; - untouched subtree identity survives a narrow replacement; - no-op intent can produce admission evidence without a new head or rewrite; - text-window reads cite a basis head and byte range; @@ -527,12 +598,12 @@ These are not first-slice requirements, but they should stay visible: ### 15. Things Not To Do -- Do not optimize the full-snapshot runtime as a substitute for graph-backed +- Avoid optimizing the full-snapshot runtime as a substitute for graph-backed authority. -- Do not let names outrun facts. A `RopeHead` must point to an actual rope. -- Do not make UI truthier than storage truth. -- Do not mix cache invalidation with authority mutation. -- Do not make compaction destroy explainability by accident. +- Keep names behind facts. A `RopeHead` must point to an actual rope. +- Keep UI claims no more authoritative than storage truth. +- Separate cache invalidation from authority mutation. +- Preserve explainability when compaction policy deletes or cold-stores evidence. ### 16. Treat UI Work As Dependent On Runtime Honesty From a302437eb5aa27009696d661ddbe48a561b9f125 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 09:24:10 -0700 Subject: [PATCH 04/16] Fix graph runtime design gate review --- docs/BEARING.md | 16 +- ...149-graph-backed-rope-runtime-discovery.md | 389 +++++++++++++++++- 2 files changed, 392 insertions(+), 13 deletions(-) diff --git a/docs/BEARING.md b/docs/BEARING.md index e6d768fb..18167c8f 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -32,6 +32,10 @@ history, merged pull requests, release notes, and design docs. repeat, transformed-repeat metadata, case operators, joins, and local marks. - Graft-backed source highlighting consumes Graft 0.10.0 projections, including plain-text prose spans when `colorful >= 0.2.1` is available on `PATH`. +- Graph-backed rope runtime authority is now an active hard gate. Do not begin + new UI causal-honesty work until + [`HT-0149 - Graph-Backed Rope Runtime Discovery`](design/0149-graph-backed-rope-runtime-discovery.md) + proves a real graph-backed create/read/replace/checkpoint path. ## Source Of Truth @@ -106,18 +110,21 @@ Infrastructure and hygiene are the third lane. Keep these scoped: Immediate order: -1. Lock +1. Land + [`HT-0149 - Graph-Backed Rope Runtime Discovery`](design/0149-graph-backed-rope-runtime-discovery.md) + as the active runtime authority gate before more causal UI posture work. +2. Lock [`WF-0108A - :why Observation Evidence Roadmap`](design/0108a-why-observation-evidence-roadmap.md) and the cross-repo issue topology. -2. Keep +3. Keep [`WF-0106 - Emacs Ideas To Steal Causally`](design/0106-emacs-ideas-to-steal-causally.md) as the supporting product-surface packet for command catalog, describe, register, macro, buffer, diagnostics, and trace surfaces. -3. Close the `:why` evidence gap in this order: local observation coordinate +4. Close the `:why` evidence gap in this order: local observation coordinate model, typed evidence obstructions, text-window evidence fields, Echo ReadingEnvelope identity, Supported Outcome Settlement outcome vocabulary, range-at-head rope history, then golden command witnesses. -4. Use existing proven Vim operations first: `dw`, `ciw`, `dd`, and `gUap`. +5. Use existing proven Vim operations first: `dw`, `ciw`, `dd`, and `gUap`. Keep `n`/`N` and `:%s` for later slices once search entry and proposal preview are product-complete. @@ -181,6 +188,7 @@ edits, agent witnesses, structural objects, or user trust. | Emacs ideas to steal causally | [`docs/design/0106-emacs-ideas-to-steal-causally.md`](design/0106-emacs-ideas-to-steal-causally.md) | | Jim command provenance and `:why` | [`docs/design/0108-causal-command-provenance-surface.md`](design/0108-causal-command-provenance-surface.md) | | `:why` observation evidence | [`docs/design/0108a-why-observation-evidence-roadmap.md`](design/0108a-why-observation-evidence-roadmap.md) | +| Graph-backed rope runtime gate | [`docs/design/0149-graph-backed-rope-runtime-discovery.md`](design/0149-graph-backed-rope-runtime-discovery.md) | | Strand/braid worldline UX | [`docs/design/0121-strand-braid-worldline-ux.md`](design/0121-strand-braid-worldline-ux.md) | | Optimistic strand worldline phases | [`docs/design/0146-optimistic-strand-worldline-phases.md`](design/0146-optimistic-strand-worldline-phases.md) | | Unmaterialized file frontier | [`docs/design/0147-unmaterialized-file-frontier.md`](design/0147-unmaterialized-file-frontier.md) | diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index 480b82b2..3a76f287 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -1,10 +1,49 @@ -# Graph-Backed Rope Runtime Discovery +--- +title: "HT-0149 - Graph-Backed Rope Runtime Discovery" +legend: "HT" +lane: "design" +issue: "https://github.com/flyingrobots/jedit/issues/206" +status: "active" +owners: + - "@flyingrobots" +created: "2026-07-04" +updated: "2026-07-04" +--- -Status: discovery summary and design gate +# HT-0149 - Graph-Backed Rope Runtime Discovery -Date: 2026-07-04 +## Linked Issue -## Summary +- https://github.com/flyingrobots/jedit/issues/206 + +## Decision Summary + +jedit's production text authority must move from retained full-text snapshots to +a graph-backed rope runtime whose heads, nodes, blobs, rewrites, diffs, +checkpoints, and receipts are admitted as inspectable causal facts. Until a real +graph-backed create/read/replace/checkpoint path exists, new UI causal-honesty +work must not claim storage truth that the runtime cannot prove. + +## Sponsored Human + +A Jim daily driver wants edits, saves, history, and `:why` explanations to remain +fast and causally trustworthy on real files, without depending on retained +full-buffer snapshots that make the editor slower and less honest as files grow. + +## Sponsored Agent + +An agent needs stable graph facts, basis IDs, byte ranges, receipts, and witness +APIs so it can inspect text authority and explain changes without scraping +rendered projections or inferring private runtime state. + +## Hill + +By the end of this cycle, jedit can create a graph-backed buffer, read a bounded +text window, replace a narrow byte range, checkpoint the resulting head, and prove +through witnesses that unchanged rope structure and authoritative bytes were not +duplicated as full snapshots. + +## Current Truth An external audit called out a real architectural drift in jedit's current text runtime. The blunt version is correct: the code currently named around @@ -91,7 +130,74 @@ The existing docs already point in the right direction. In particular: The problem is that implementation reality has not caught up to those design claims. -## Planned Response +## Problem + +The installed jedit text authority still admits edits by copying and retaining +whole materialized strings. That contradicts the graph-backed rope architecture, +turns rope/worldline names into misleading labels, makes byte-range provenance +harder to prove, and allows future UI work to present causal claims that the +storage layer cannot support. + +## Scope + +This cycle includes: + +- documenting the current full-snapshot runtime as fixture-only; +- defining the production guard that prevents implicit snapshot authority; +- defining authoritative UTF-8 byte coordinates and projection coordinates; +- defining concrete graph-backed rope fact shapes and validation boundaries; +- defining byte authority for `TextBlob` facts; +- defining no-op admission semantics without minting text ticks; +- defining retention, subtree identity, materialization, checkpoint, and `:why` + witnesses; +- linking the active repo bearing to this hard gate. + +## Non-Goals + +This cycle does not include: + +- implementing the final production graph-backed runtime; +- optimizing the full-snapshot runtime; +- changing rendered editor UI, gutter UI, footer UI, or settings UI; +- solving compaction, braids, collaborative merges, search indexes, syntax + caches, or structural highlighting; +- moving jedit text semantics into Echo core. + +## User Experience / Product Shape + +This design does not add a new rendered surface. The user-facing impact is a +hard execution gate: Jim should avoid additional causal-honesty UI claims until +runtime truth can back those claims with graph facts. + +### User Journey + +```mermaid +flowchart TD + Start[User edits a buffer] --> Admit[Runtime admits text intent] + Admit --> Facts[Graph-backed rope facts evolve] + Facts --> Read[UI reads bounded projection from a basis head] + Read --> Explain[:why cites head, leaf, blob, rewrite, diff, and tick] + Admit --> NoOp[No-op admission] + NoOp --> Receipt[Receipt without new text head or tick] +``` + +### Wide UI Mockup + +Not applicable. This cycle changes runtime authority design and process +signposting, not rendered TUI layout. + +### Narrow UI Mockup + +Not applicable. This cycle changes runtime authority design and process +signposting, not rendered TUI layout. + +### Accessibility Considerations + +No rendered accessibility behavior changes in this cycle. Future UI work that +uses this runtime must expose the same causal facts through keyboard-accessible +commands and machine-readable witnesses, not color-only or pixel-only cues. + +## Runtime / API Contract We should not patch this by making full-string replacement faster. That would preserve the wrong architecture. The response needs to be a graph-backed runtime @@ -244,6 +350,19 @@ interface RopeLeafFact { readonly contentHash: Hash; } +interface InlineTextBlobStorage { + readonly kind: "inline-utf8-bytes"; + readonly bytes: Uint8Array; +} + +interface StoredTextBlobStorage { + readonly kind: "content-addressed-blob-store"; + readonly storeId: "jedit.text.blob-store.v1"; + readonly contentRef: string; +} + +type TextBlobStorage = InlineTextBlobStorage | StoredTextBlobStorage; + interface TextBlobFact { readonly kind: "jedit.text.TextBlob"; readonly schemaVersion: 1; @@ -251,6 +370,7 @@ interface TextBlobFact { readonly encoding: "utf8"; readonly byteLength: number; readonly contentHash: Hash; + readonly storage: TextBlobStorage; } ``` @@ -304,6 +424,12 @@ Validation rules: the same write set or an already admitted basis; - `TextBlobFact.blobId` and `contentHash` must be derived from `encoding + bytes`, not trusted from caller input; +- inline blob facts must compute hash and length from their `Uint8Array` bytes; +- blob-store-backed facts must name the store adapter and content reference, and + admission must fetch bytes, verify length, and recompute the hash before the + fact can become authority; +- a `textWindow` read over a missing or hash-mismatched blob is an obstruction, + not a fallback to stale projection text; - branch, leaf, and head hashes must be recomputed from child/blob references and metrics before admission; - invalid facts are rejected before Echo admission and never become retained @@ -319,17 +445,25 @@ receipt. The design should distinguish: - `ReplaceRangeIntent`; -- `ReplaceRangeAdmission`; +- `TextChangeAdmission`; +- `NoOpAdmissionReceipt`; +- `RejectedIntentReceipt`; - `RopeRewrite | null`; - `RopeDiff | null`; - `WorldlineAdvance | null`; -- `TickReceipt`. +- `TickReceipt | null`. Rules: - no text change means no new `RopeHead`; - no changed range means no `RopeRewrite`; -- optional admission or receipt evidence may still exist; +- no text change means no new text tick, no tick sequence advance, and no reuse + of a prior `TickId`; +- `TickReceipt` exists only for a text-changing admission that advances the + worldline; +- `NoOpAdmissionReceipt` records request ID, basis head, range, replacement hash, + and reason such as `unchanged-bytes`, but it is not a text tick; +- rejected intents use `RejectedIntentReceipt` and also do not mint text ticks; - no-op evidence must not pollute the rope graph as if bytes changed. ### 5. Make Untouched Subtree Identity A Contract @@ -612,7 +746,122 @@ checked against the runtime truth. If the UI says "basis", "head", "tick", "checkpoint", or "worldline", the source underneath should be graph-backed causal evidence or explicitly marked as a transitional projection. -## Immediate Decision +## Lower Modes + +The runtime contract must remain inspectable without a full TUI session: + +- tests can inject the full-snapshot fixture only through an explicit + fixture-allowing path; +- debug and witness APIs can emit deterministic JSON for rope shape, retained + bytes, materialization basis, no-op receipts, and checkpoints; +- missing Echo, Graft, filesystem, or blob-store evidence produces typed + obstructions instead of silently falling back to projection text; +- terminal size, color, and localization do not affect graph fact authority. + +## Data / State Model + +| Category | Description | +| --- | --- | +| Source of truth | Echo-admitted jedit rope facts: worldlines, heads, nodes, blobs, rewrites, diffs, checkpoints, and receipts. | +| Derived state | Materialized text windows, line offset indexes, syntax spans, render layout, search indexes, and UI caches. | +| Invalid states | A head without a root node, a leaf without verified blob bytes, a branch with mismatched metrics or hash, a no-op that advances a text tick, and product startup that silently uses the snapshot fixture. | +| Reset behavior | Rebuild derived indexes from a named head or checkpoint. Do not rebuild authority from rendered lines or cached projections. | +| Serialization | Graph facts and blob-store entries serialize with runtime `kind`, `schemaVersion`, IDs, byte metrics, references, and hashes. | +| Deterministic assumptions | UTF-8 bytes are storage authority; line/column and UTF-16 positions are basis-bound projections; hash and metric validation is deterministic. | + +```mermaid +stateDiagram-v2 + [*] --> SnapshotFixtureQuarantined + SnapshotFixtureQuarantined --> GraphFactsDefined + GraphFactsDefined --> WitnessesFailing + WitnessesFailing --> TinyGraphRuntime + TinyGraphRuntime --> ProductionCutover +``` + +## Accessibility Posture + +| Concern | Posture | +| --- | --- | +| Semantic labels or facts | Runtime truth is exposed as graph facts and deterministic witness output. | +| Focus order or ownership | Not changed by this design cycle. | +| Hidden or visual-only information | Causal state must not be available only through color, gutter marks, or footer prose. | +| Keyboard behavior | Not changed by this design cycle. | +| Secret or redaction behavior | Blob witnesses should support redacted byte previews while retaining hashes and byte ranges. | + +## Localization / Directionality Posture + +| Concern | Posture | +| --- | --- | +| User-visible strings | No new runtime strings beyond guard and obstruction messages. | +| Catalog keys | Not required for this design-only cycle. | +| Supported locales updated | Not required. | +| Directionality assumptions | Text storage uses UTF-8 byte order; UI directionality is a projection concern. | +| Validation command | `npx markdownlint-cli2 docs/design/0149-graph-backed-rope-runtime-discovery.md` | + +## Agent Inspectability / Explainability Posture + +Agents must be able to inspect the result through stable IDs and witness APIs: + +- `debugRopeShape(headId)` exposes head ID, root ID, spans, node IDs, child IDs, + hashes, depth, retained blob bytes, and materialized projection bytes; +- `textWindow({ basisHeadId, byteRange })` returns text with basis head, byte + range, cache status, and validation evidence; +- no-op admissions emit non-ticking receipt objects; +- `:why` acceptance cites head, leaf, blob, rewrite, diff, tick, checkpoint, and + basis evidence for a byte range. + +## Linked Invariants + +- Runtime truth beats type theater. +- Materialization is a reading, not reality. +- A rope runtime is defined by what survives an edit. +- Echo remains generic and does not learn jedit text semantics. +- UI causal claims must not outrun storage authority. +- Tests and witnesses are executable spec. + +## Design Alternatives Considered + +### Option A: Optimize The Snapshot Runtime + +Pros: + +- Smaller immediate code change. +- Could reduce short-term latency for small files. + +Cons: + +- Preserves the wrong authority model. +- Keeps full-buffer retention as production truth. +- Lets misleading rope/worldline names continue to outrun facts. + +### Option B: Move Directly To A Complete Rope Runtime + +Pros: + +- Reaches the intended architecture in one broad effort. +- Avoids intermediate fixture quarantine work. + +Cons: + +- Too large to review or witness safely. +- Risks mixing compaction, braids, UI, syntax, and retention before the minimal + create/read/replace/checkpoint path is proven. + +### Option C: Fence The Fixture And Build The Smallest Real Path + +Pros: + +- Makes the unsafe authority explicit immediately. +- Lets witnesses fail before implementation. +- Proves graph-backed create/read/replace/checkpoint before UI posture depends on + it. + +Cons: + +- Leaves some current dogfood discomfort in place while the real runtime lands. +- Requires transitional compatibility until production cutover finishes. + +## Decision Do not treat the current full-snapshot hot text runtime as an acceptable production implementation. It can remain only as a bounded fixture while the @@ -634,3 +883,125 @@ A rope runtime is defined by what survives an edit. Materialization is a reading, not reality. Causal honesty is an end-to-end property. ``` + +## Implementation Slices + +- [ ] Slice 1: Rename the snapshot runtime as a full-snapshot fixture and add the + production guard. +- [ ] Slice 2: Add a quarantine witness proving default product construction + cannot silently use the fixture. +- [ ] Slice 3: Land coordinate, fact, byte-authority, and validation contracts. +- [ ] Slice 4: Add failing retention, subtree identity, materialization, no-op, + save/export, and `:why` witnesses. +- [ ] Slice 5: Implement graph-backed `createBufferWorldline` and `textWindow`. +- [ ] Slice 6: Implement graph-backed single-range `replaceRangeAsTick`. +- [ ] Slice 7: Implement graph-backed `createCheckpoint` and cut product + construction over to graph-backed authority. + +## Tests To Write First + +Behavior tests required: + +- [ ] Product construction rejects implicit `FullSnapshotHotTextRuntimeFixture`. +- [ ] Repeated small edits on a large buffer do not retain O(buffer size * edit + count) authoritative bytes. +- [ ] Narrow replacement preserves untouched subtree identity recursively. +- [ ] No-op replacement emits no new head, rewrite, diff, worldline advance, or + text tick. +- [ ] `textWindow` returns basis head, UTF-8 byte range, cache status, and hash + validation evidence. +- [ ] Save/export reads from a named head or checkpoint without mutating text + authority. +- [ ] `:why` can cite head, leaf, blob, rewrite, diff, tick, checkpoint, and + basis evidence for a byte range. + +Documentation and process tests: + +- [ ] Design-cycle policy continues to recognize the required template headings. +- [ ] BEARING links this runtime gate while it blocks UI causal-honesty work. + +## Acceptance Criteria + +The work is done when: + +- [ ] The full-snapshot runtime cannot be installed as default production text + authority without an explicit fixture escape hatch. +- [ ] A graph-backed runtime can create, read, replace, and checkpoint one buffer. +- [ ] Retention, subtree identity, no-op, materialization, save/export, and `:why` + witnesses pass against graph-backed authority. +- [ ] UI surfaces that mention basis, head, tick, checkpoint, or worldline cite + graph facts or explicitly mark transitional projection posture. +- [ ] Issue #206 and PR #205 are linked correctly. +- [ ] CI and local validation are green. + +## Validation Plan + +Commands expected before implementation PRs: + +```bash +git diff --check +npx markdownlint-cli2 docs/BEARING.md docs/design/0149-graph-backed-rope-runtime-discovery.md +node --test --test-concurrency=1 spec/design-cycle-policy.spec.mjs +npm run quality +``` + +Runtime implementation slices should also run focused behavior witnesses and the +full `npm run check` before merge. + +## Playback / Witness + +Reviewers can inspect: + +```bash +sed -n '1,260p' docs/design/0149-graph-backed-rope-runtime-discovery.md +sed -n '1,220p' docs/BEARING.md +node --test --test-concurrency=1 spec/design-cycle-policy.spec.mjs +``` + +Future runtime PRs should add machine-readable witness output for retained bytes, +debug rope shape, text-window basis, checkpoint basis, and `:why` byte-range +evidence. + +## Risks + +Known risks: + +- The snapshot fixture could remain wired into product code too long. +- A graph-backed runtime could materialize full strings internally and still pass + superficial read tests. +- Blob-store-backed facts could become unverifiable if byte retrieval and hash + checks are optional. +- UI work could resume causal language before runtime authority is ready. + +Mitigations: + +- Keep the fixture name and guard intentionally loud. +- Make retention and untouched subtree witnesses required implementation proof. +- Treat missing or mismatched blob bytes as obstructions. +- Keep BEARING pointed at this gate until create/read/replace/checkpoint lands. + +## Follow-On Debt + +- Issue #206 tracks the runtime gate and implementation slices. +- Follow-up runtime PRs should create narrower issues for compaction, + rebalancing policy, `:why` inspector UI, retention dashboards, and migration + from snapshot fixture state. + +## Retrospective + +What changed from the design: + +- This PR is the design gate and does not implement graph-backed authority. + +What the tests proved: + +- Markdown structure, design-cycle policy, ASCII hygiene, and the repo quality + gate pass for the design packet. + +What remains open: + +- The implementation slices in issue #206 remain open. + +PR: + +- https://github.com/flyingrobots/jedit/pull/205 From c648469f798f30aeea312a5f62ff9b2cb6af5fc7 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 09:29:59 -0700 Subject: [PATCH 05/16] Fix: pin design current truth evidence --- ...149-graph-backed-rope-runtime-discovery.md | 20 +++++++-------- spec/design-cycle-policy.spec.mjs | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index 3a76f287..9da0fe7b 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -52,14 +52,14 @@ implement a graph-backed rope runtime. The current hot text runtime still uses full-text snapshots: -- [`src/domain/text-edit-contract.ts`](../../src/domain/text-edit-contract.ts) +- [`src/domain/text-edit-contract.ts`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/src/domain/text-edit-contract.ts#L84) encodes the entire buffer into UTF-8 bytes, splices the requested range, decodes a new full string, and wraps that string in a new `BufferRoot`. -- [`src/ports/hot-text-runtime.ts`](../../src/ports/hot-text-runtime.ts) +- [`src/ports/hot-text-runtime.ts`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/src/ports/hot-text-runtime.ts#L6) defines `HotTextBufferState.roots` as an array of retained roots. -- [`src/adapters/in-memory-hot-text-runtime.ts`](../../src/adapters/in-memory-hot-text-runtime.ts) +- [`src/adapters/in-memory-hot-text-runtime.ts`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/src/adapters/in-memory-hot-text-runtime.ts#L76) appends the new full root to that retained array after an admitted edit. -- [`src/adapters/installed-jedit-contract-echo-transport.ts`](../../src/adapters/installed-jedit-contract-echo-transport.ts) +- [`src/adapters/installed-jedit-contract-echo-transport.ts`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/src/adapters/installed-jedit-contract-echo-transport.ts#L159) still defaults the installed jedit contract transport to that in-memory runtime. @@ -116,15 +116,15 @@ ways: The existing docs already point in the right direction. In particular: -- [`0003-echo-backed-rope-worldline-contract.md`](0003-echo-backed-rope-worldline-contract/echo-backed-rope-worldline-contract.md) +- [`0003-echo-backed-rope-worldline-contract.md`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/docs/design/0003-echo-backed-rope-worldline-contract/echo-backed-rope-worldline-contract.md#L28) says witnessed causal history is canonical and materialized projections are not editor truth. -- [`jedit-echo-graph-model.md`](jedit-echo-graph-model.md) describes the desired - `BufferWorldline -> RopeHead -> Rope DAG` shape and states that - `ReplaceRangeAsTick` should reuse untouched subtrees. -- [`0027-echo-hosted-production-cutover.md`](0027-echo-hosted-production-cutover.md) +- [`jedit-echo-graph-model.md`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/docs/design/jedit-echo-graph-model.md#L31) + describes the desired `BufferWorldline -> RopeHead -> Rope DAG` shape and + states that `ReplaceRangeAsTick` should reuse untouched subtrees. +- [`0027-echo-hosted-production-cutover.md`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/docs/design/0027-echo-hosted-production-cutover.md#L35) says the local in-memory text model is no longer a production authority target. -- [`structural-history-graphql-authority.md`](structural-history-graphql-authority.md) +- [`structural-history-graphql-authority.md`](https://github.com/flyingrobots/jedit/blob/e93b2e1a138a762d7a33da6179d3ad8b8b2a9c6e/docs/design/structural-history-graphql-authority.md#L11) says the TypeScript model is transitional evidence, not durable authority. The problem is that implementation reality has not caught up to those design diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index 760538a7..d230d49d 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -14,6 +14,12 @@ const WHY_OBSERVATION_ROADMAP_PATH = path.join( 'design', '0108a-why-observation-evidence-roadmap.md', ); +const GRAPH_RUNTIME_DISCOVERY_PATH = path.join( + REPO_ROOT, + 'docs', + 'design', + '0149-graph-backed-rope-runtime-discovery.md', +); const REQUIRED_TEMPLATE_HEADINGS = Object.freeze([ '## Linked Issue', @@ -74,6 +80,25 @@ test('WF-0108A roadmap preserves required full-cycle design headings', () => { } }); +test('HT-0149 runtime discovery pins Current Truth evidence to git SHAs', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + const sha = '[0-9a-f]{40}'; + + for (const repoPath of [ + 'src/domain/text-edit-contract.ts', + 'src/ports/hot-text-runtime.ts', + 'src/adapters/in-memory-hot-text-runtime.ts', + 'src/adapters/installed-jedit-contract-echo-transport.ts', + 'docs/design/jedit-echo-graph-model.md', + ]) { + assert.match( + discovery, + new RegExp(`https://github\\.com/flyingrobots/jedit/blob/${sha}/${escapeRegExp(repoPath)}#L\\d+`), + `${repoPath} needs a pinned Current Truth evidence link`, + ); + } +}); + test('process doc defines the official cycle lifecycle and proof boundary', () => { const processDoc = readRepoFile(PROCESS_PATH); From 863bf727433971219b420f53270a71c2b7c05f10 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 10:30:16 -0700 Subject: [PATCH 06/16] Fix: pass validation context to rope facts --- ...149-graph-backed-rope-runtime-discovery.md | 25 ++++++++++++++++--- spec/design-cycle-policy.spec.mjs | 13 ++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index 9da0fe7b..1acfda5c 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -405,8 +405,25 @@ type FactValidationResult = | { readonly ok: true; readonly fact: TFact } | { readonly ok: false; readonly code: FactValidationErrorCode }; +interface RopeFactReadModel { + hasFact(id: string): boolean; +} + +interface TextBlobStorePort { + readBlobBytes(storage: StoredTextBlobStorage): Uint8Array | null; +} + +interface RopeFactValidationContext { + readonly writeSet: readonly object[]; + readonly admittedBasis: RopeFactReadModel; + readonly blobStore: TextBlobStorePort; +} + declare function makeTextBlobFact(bytes: Uint8Array): TextBlobFact; -declare function validateRopeFact(payload: object): FactValidationResult< +declare function validateRopeFact( + payload: object, + context: RopeFactValidationContext, +): FactValidationResult< | BufferWorldlineFact | RopeHeadFact | RopeBranchFact @@ -422,12 +439,14 @@ Validation rules: - numeric metrics must be non-negative integers; - branch children, head roots, and leaf blobs must reference facts available in the same write set or an already admitted basis; +- the validator receives those scopes through `RopeFactValidationContext` and + must not consult ambient process state; - `TextBlobFact.blobId` and `contentHash` must be derived from `encoding + bytes`, not trusted from caller input; - inline blob facts must compute hash and length from their `Uint8Array` bytes; - blob-store-backed facts must name the store adapter and content reference, and - admission must fetch bytes, verify length, and recompute the hash before the - fact can become authority; + admission must fetch bytes through `context.blobStore`, verify length, and + recompute the hash before the fact can become authority; - a `textWindow` read over a missing or hash-mismatched blob is an obstruction, not a fallback to stale projection text; - branch, leaf, and head hashes must be recomputed from child/blob references and diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index d230d49d..07b7d9c9 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -99,6 +99,19 @@ test('HT-0149 runtime discovery pins Current Truth evidence to git SHAs', () => } }); +test('HT-0149 rope fact validation receives admission context', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + + assert.match(discovery, /^interface RopeFactValidationContext \{$/m); + assert.match(discovery, /readonly writeSet:/); + assert.match(discovery, /readonly admittedBasis:/); + assert.match(discovery, /readonly blobStore:/); + assert.match( + discovery, + /declare function validateRopeFact\(\n payload: object,\n context: RopeFactValidationContext,\n\): FactValidationResult { const processDoc = readRepoFile(PROCESS_PATH); From 5c5088ca34ef00326f9a4f3806cf04e4232dc2fd Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 10:31:50 -0700 Subject: [PATCH 07/16] Fix: define rewrite diff tick facts --- ...149-graph-backed-rope-runtime-discovery.md | 65 +++++++++++++++++-- spec/design-cycle-policy.spec.mjs | 14 ++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index 1acfda5c..0759ee69 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -304,8 +304,16 @@ type RopeHeadId = string & { readonly __brand: "RopeHeadId" }; type RopeNodeId = string & { readonly __brand: "RopeNodeId" }; type TextBlobId = string & { readonly __brand: "TextBlobId" }; type TickId = string & { readonly __brand: "TickId" }; +type RopeRewriteId = string & { readonly __brand: "RopeRewriteId" }; +type RopeDiffId = string & { readonly __brand: "RopeDiffId" }; +type AdmissionId = string & { readonly __brand: "AdmissionId" }; type Hash = string & { readonly __brand: "Hash" }; +interface TextByteRange { + readonly startByte: ByteOffset; + readonly endByte: ByteOffset; +} + interface BufferWorldlineFact { readonly kind: "jedit.text.BufferWorldline"; readonly schemaVersion: 1; @@ -372,13 +380,55 @@ interface TextBlobFact { readonly contentHash: Hash; readonly storage: TextBlobStorage; } + +interface RopeRewriteFact { + readonly kind: "jedit.text.RopeRewrite"; + readonly schemaVersion: 1; + readonly rewriteId: RopeRewriteId; + readonly worldlineId: WorldlineId; + readonly basisHeadId: RopeHeadId; + readonly nextHeadId: RopeHeadId; + readonly admittedByTickId: TickId; + readonly range: TextByteRange; + readonly replacementBlobId: TextBlobId; + readonly diffId: RopeDiffId; + readonly contentHash: Hash; +} + +interface RopeDiffSpan { + readonly kind: "equal" | "delete" | "insert"; + readonly basisRange?: TextByteRange; + readonly nextRange?: TextByteRange; + readonly blobId?: TextBlobId; + readonly contentHash: Hash; +} + +interface RopeDiffFact { + readonly kind: "jedit.text.RopeDiff"; + readonly schemaVersion: 1; + readonly diffId: RopeDiffId; + readonly rewriteId: RopeRewriteId; + readonly basisHeadId: RopeHeadId; + readonly nextHeadId: RopeHeadId; + readonly spans: readonly RopeDiffSpan[]; + readonly contentHash: Hash; +} + +interface TickReceiptFact { + readonly kind: "jedit.text.TickReceipt"; + readonly schemaVersion: 1; + readonly tickId: TickId; + readonly admissionId: AdmissionId; + readonly worldlineId: WorldlineId; + readonly basisHeadId: RopeHeadId; + readonly nextHeadId: RopeHeadId; + readonly rewriteId: RopeRewriteId; + readonly admittedAtSequence: number; +} ``` The full design must also define facts for: -- `RopeRewrite`; -- `RopeDiff`; -- `TickReceipt`; - `RopeCheckpoint`; - anchors; - strands, braids, and admissions when their implementation slice begins. @@ -429,6 +479,9 @@ declare function validateRopeFact( | RopeBranchFact | RopeLeafFact | TextBlobFact + | RopeRewriteFact + | RopeDiffFact + | TickReceiptFact >; ``` @@ -437,8 +490,8 @@ Validation rules: - `kind` and `schemaVersion` are mandatory runtime tags; - IDs must be non-empty canonical IDs in the expected namespace; - numeric metrics must be non-negative integers; -- branch children, head roots, and leaf blobs must reference facts available in - the same write set or an already admitted basis; +- branch children, head roots, leaf blobs, rewrites, diffs, and receipts must + reference facts available in the same write set or an already admitted basis; - the validator receives those scopes through `RopeFactValidationContext` and must not consult ambient process state; - `TextBlobFact.blobId` and `contentHash` must be derived from @@ -451,6 +504,8 @@ Validation rules: not a fallback to stale projection text; - branch, leaf, and head hashes must be recomputed from child/blob references and metrics before admission; +- rewrite, diff, and tick receipt hashes and sequence numbers must be recomputed + or range-checked against the admitted basis before admission; - invalid facts are rejected before Echo admission and never become retained authority. diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index 07b7d9c9..12407c30 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -112,6 +112,20 @@ test('HT-0149 rope fact validation receives admission context', () => { ); }); +test('HT-0149 defines rewrite diff and tick receipt facts', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + + for (const factName of ['RopeRewriteFact', 'RopeDiffFact', 'TickReceiptFact']) { + assert.match(discovery, new RegExp(`^interface ${factName} \\{$`, 'm')); + } + + assert.match(discovery, /^interface TextByteRange \{$/m); + assert.match(discovery, /readonly range: TextByteRange;/); + assert.match(discovery, /readonly diffId: RopeDiffId;/); + assert.match(discovery, /readonly spans: readonly RopeDiffSpan\[\];/); + assert.match(discovery, /readonly admittedAtSequence: number;/); +}); + test('process doc defines the official cycle lifecycle and proof boundary', () => { const processDoc = readRepoFile(PROCESS_PATH); From 58d7f11725e5dce6a219d7422ad734cf714ef59c Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 10:33:09 -0700 Subject: [PATCH 08/16] Fix: version checkpoint fact shape --- docs/design/0149-graph-backed-rope-runtime-discovery.md | 5 ++++- spec/design-cycle-policy.spec.mjs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index 0759ee69..08794a40 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -736,9 +736,12 @@ Checkpoint semantics also need precision. A checkpoint is not new text truth. It is a durable named basis for efficient future reads, retention, or export. ```typescript +type CheckpointId = string & { readonly __brand: "CheckpointId" }; + interface RopeCheckpointFact { readonly kind: "jedit.text.RopeCheckpoint"; - readonly checkpointId: string; + readonly schemaVersion: 1; + readonly checkpointId: CheckpointId; readonly worldlineId: WorldlineId; readonly headId: RopeHeadId; readonly createdByTickId: TickId; diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index 12407c30..9757d220 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -126,6 +126,12 @@ test('HT-0149 defines rewrite diff and tick receipt facts', () => { assert.match(discovery, /readonly admittedAtSequence: number;/); }); +test('HT-0149 checkpoint fact carries schema version', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + + assert.match(discovery, /interface RopeCheckpointFact \{[\s\S]*readonly schemaVersion: 1;/); +}); + test('process doc defines the official cycle lifecycle and proof boundary', () => { const processDoc = readRepoFile(PROCESS_PATH); From ce8477b460b8f1d9f245a0a19a9316453f0659b9 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 10:34:10 -0700 Subject: [PATCH 09/16] Fix: specify rope balance invariants --- ...0149-graph-backed-rope-runtime-discovery.md | 18 ++++++++++++++++++ spec/design-cycle-policy.spec.mjs | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index 08794a40..1ad8a889 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -732,6 +732,24 @@ Recommended posture: - both can be retained; - normal UI hides structural maintenance unless debugging. +Initial balance invariant for the tiny graph-backed runtime: + +- target leaf byte length is 4096 bytes; +- minimum non-edge leaf byte length is 1024 bytes; +- edge leaves may be smaller than the minimum when the buffer itself is smaller + or when the edge range has no merge candidate; +- maximum leaf byte length is 8192 bytes before a replacement must split or + rebalance; +- branches store exact `byteLength`, `lineCount`, `height`, and `contentHash` + aggregates from children; +- branch height difference must be no greater than 1 after maintenance, except + while one admitted rewrite is still emitting its structural-maintenance facts; +- replacement triggers rebalance when a touched leaf exceeds 8192 bytes, a + non-edge leaf falls below 1024 bytes and has a merge candidate, or a copied + path would create a branch height difference greater than 1; +- rebalancing emits structural-maintenance evidence linked to the semantic + rewrite that made maintenance necessary. + Checkpoint semantics also need precision. A checkpoint is not new text truth. It is a durable named basis for efficient future reads, retention, or export. diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index 9757d220..c9f305ad 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -132,6 +132,15 @@ test('HT-0149 checkpoint fact carries schema version', () => { assert.match(discovery, /interface RopeCheckpointFact \{[\s\S]*readonly schemaVersion: 1;/); }); +test('HT-0149 specifies concrete rope balance invariants', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + + assert.match(discovery, /target leaf byte length is 4096 bytes/); + assert.match(discovery, /minimum non-edge leaf byte length is 1024 bytes/); + assert.match(discovery, /maximum leaf byte length is 8192 bytes/); + assert.match(discovery, /branch height difference must be no greater than 1/); +}); + test('process doc defines the official cycle lifecycle and proof boundary', () => { const processDoc = readRepoFile(PROCESS_PATH); From 9fda0507c4be225814412ff52877e8ed1c122aa3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 10:36:42 -0700 Subject: [PATCH 10/16] Fix: link graph runtime follow-on debt --- ...149-graph-backed-rope-runtime-discovery.md | 16 ++++++++++++---- spec/design-cycle-policy.spec.mjs | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index 1ad8a889..cb25a5f5 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -1077,10 +1077,18 @@ Mitigations: ## Follow-On Debt -- Issue #206 tracks the runtime gate and implementation slices. -- Follow-up runtime PRs should create narrower issues for compaction, - rebalancing policy, `:why` inspector UI, retention dashboards, and migration - from snapshot fixture state. +- [#206](https://github.com/flyingrobots/jedit/issues/206) tracks the runtime + gate and implementation slices. +- [#207](https://github.com/flyingrobots/jedit/issues/207) tracks graph-backed + rope compaction policy. +- [#208](https://github.com/flyingrobots/jedit/issues/208) tracks graph-backed + rope rebalancing evidence. +- [#209](https://github.com/flyingrobots/jedit/issues/209) tracks the rope fact + inspector and range `:why` UI. +- [#210](https://github.com/flyingrobots/jedit/issues/210) tracks the retention + budget dashboard. +- [#211](https://github.com/flyingrobots/jedit/issues/211) tracks snapshot + fixture import migration. ## Retrospective diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index c9f305ad..bcd6eb13 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -141,6 +141,15 @@ test('HT-0149 specifies concrete rope balance invariants', () => { assert.match(discovery, /branch height difference must be no greater than 1/); }); +test('HT-0149 follow-on debt is issue-backed', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + const followOnDebt = sectionBetween(discovery, '## Follow-On Debt', '## Retrospective'); + const issueLinks = followOnDebt.match(/https:\/\/github\.com\/flyingrobots\/jedit\/issues\/\d+/g) ?? []; + + assert.ok(issueLinks.length >= 6, 'follow-on debt should link tracker issues for every deferred item'); + assert.doesNotMatch(followOnDebt, /should create narrower issues/); +}); + test('process doc defines the official cycle lifecycle and proof boundary', () => { const processDoc = readRepoFile(PROCESS_PATH); @@ -174,6 +183,16 @@ test('policy design links the GitHub issue and records the retrospective', () => assert.match(policy, /Other repositories still need their own adapted templates/); }); +function sectionBetween(documentText, startHeading, endHeading) { + const start = documentText.indexOf(startHeading); + const end = documentText.indexOf(endHeading, start + startHeading.length); + + assert.notEqual(start, -1, `${startHeading} missing`); + assert.notEqual(end, -1, `${endHeading} missing`); + + return documentText.slice(start, end); +} + function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } From 84968eedb5b1d03b36933371bd37c3fa1515a61a Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 12:17:22 -0700 Subject: [PATCH 11/16] Fix: validate checkpoint rope facts --- .../0149-graph-backed-rope-runtime-discovery.md | 11 ++++++----- spec/design-cycle-policy.spec.mjs | 8 ++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index cb25a5f5..b32080de 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -429,7 +429,6 @@ interface TickReceiptFact { The full design must also define facts for: -- `RopeCheckpoint`; - anchors; - strands, braids, and admissions when their implementation slice begins. @@ -482,6 +481,7 @@ declare function validateRopeFact( | RopeRewriteFact | RopeDiffFact | TickReceiptFact + | RopeCheckpointFact >; ``` @@ -490,8 +490,9 @@ Validation rules: - `kind` and `schemaVersion` are mandatory runtime tags; - IDs must be non-empty canonical IDs in the expected namespace; - numeric metrics must be non-negative integers; -- branch children, head roots, leaf blobs, rewrites, diffs, and receipts must - reference facts available in the same write set or an already admitted basis; +- branch children, head roots, leaf blobs, rewrites, diffs, receipts, and + checkpoints must reference facts available in the same write set or an already + admitted basis; - the validator receives those scopes through `RopeFactValidationContext` and must not consult ambient process state; - `TextBlobFact.blobId` and `contentHash` must be derived from @@ -504,8 +505,8 @@ Validation rules: not a fallback to stale projection text; - branch, leaf, and head hashes must be recomputed from child/blob references and metrics before admission; -- rewrite, diff, and tick receipt hashes and sequence numbers must be recomputed - or range-checked against the admitted basis before admission; +- rewrite, diff, tick receipt, and checkpoint hashes and sequence numbers must + be recomputed or range-checked against the admitted basis before admission; - invalid facts are rejected before Echo admission and never become retained authority. diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index bcd6eb13..0b48bccb 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -132,6 +132,14 @@ test('HT-0149 checkpoint fact carries schema version', () => { assert.match(discovery, /interface RopeCheckpointFact \{[\s\S]*readonly schemaVersion: 1;/); }); +test('HT-0149 checkpoint facts are validated rather than deferred', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + const deferredFacts = sectionBetween(discovery, 'The full design must also define facts for:', 'Echo remains generic.'); + + assert.match(discovery, /\| TickReceiptFact\n \| RopeCheckpointFact\n>/); + assert.doesNotMatch(deferredFacts, /`RopeCheckpoint`/); +}); + test('HT-0149 specifies concrete rope balance invariants', () => { const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); From 01131976aab65839fd5faef56a9d344b2a7b907e Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 21:49:34 -0700 Subject: [PATCH 12/16] Fix: block why work on rope runtime gate --- docs/BEARING.md | 6 ++++-- spec/design-cycle-policy.spec.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/BEARING.md b/docs/BEARING.md index 18167c8f..418ba3d2 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -120,8 +120,10 @@ Immediate order: [`WF-0106 - Emacs Ideas To Steal Causally`](design/0106-emacs-ideas-to-steal-causally.md) as the supporting product-surface packet for command catalog, describe, register, macro, buffer, diagnostics, and trace surfaces. -4. Close the `:why` evidence gap in this order: local observation coordinate - model, typed evidence obstructions, text-window evidence fields, Echo +4. Do not resume the `:why` evidence gap sequence until the graph-backed + create/read/replace/checkpoint path and witnesses land. When that runtime + proof exists, resume in this order: local observation coordinate model, + typed evidence obstructions, text-window evidence fields, Echo ReadingEnvelope identity, Supported Outcome Settlement outcome vocabulary, range-at-head rope history, then golden command witnesses. 5. Use existing proven Vim operations first: `dw`, `ciw`, `dd`, and `gUap`. diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index 0b48bccb..21cd4210 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -8,6 +8,7 @@ const AGENTS_PATH = path.join(REPO_ROOT, 'AGENTS.md'); const PROCESS_PATH = path.join(REPO_ROOT, 'docs', 'method', 'process.md'); const TEMPLATE_PATH = path.join(REPO_ROOT, 'docs', 'design', 'TEMPLATE.md'); const POLICY_DESIGN_PATH = path.join(REPO_ROOT, 'docs', 'design', '0034-design-cycle-template-and-lifecycle.md'); +const BEARING_PATH = path.join(REPO_ROOT, 'docs', 'BEARING.md'); const WHY_OBSERVATION_ROADMAP_PATH = path.join( REPO_ROOT, 'docs', @@ -158,6 +159,13 @@ test('HT-0149 follow-on debt is issue-backed', () => { assert.doesNotMatch(followOnDebt, /should create narrower issues/); }); +test('BEARING blocks why work on graph runtime proof', () => { + const bearing = readRepoFile(BEARING_PATH); + + assert.match(bearing, /Do not resume the `:why` evidence gap sequence until/); + assert.match(bearing, /graph-backed\s+create\/read\/replace\/checkpoint path and witnesses land/); +}); + test('process doc defines the official cycle lifecycle and proof boundary', () => { const processDoc = readRepoFile(PROCESS_PATH); From f97156002009d068b2e97c32591255e061801f62 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 21:53:46 -0700 Subject: [PATCH 13/16] Fix: expose typed rope facts to validator --- ...149-graph-backed-rope-runtime-discovery.md | 28 +++++++++++-------- spec/design-cycle-policy.spec.mjs | 14 +++++++++- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index b32080de..cd9e7a07 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -454,8 +454,19 @@ type FactValidationResult = | { readonly ok: true; readonly fact: TFact } | { readonly ok: false; readonly code: FactValidationErrorCode }; +type RopeAdmittedFact = + | BufferWorldlineFact + | RopeHeadFact + | RopeBranchFact + | RopeLeafFact + | TextBlobFact + | RopeRewriteFact + | RopeDiffFact + | TickReceiptFact + | RopeCheckpointFact; + interface RopeFactReadModel { - hasFact(id: string): boolean; + getFact(id: string): RopeAdmittedFact | null; } interface TextBlobStorePort { @@ -472,17 +483,7 @@ declare function makeTextBlobFact(bytes: Uint8Array): TextBlobFact; declare function validateRopeFact( payload: object, context: RopeFactValidationContext, -): FactValidationResult< - | BufferWorldlineFact - | RopeHeadFact - | RopeBranchFact - | RopeLeafFact - | TextBlobFact - | RopeRewriteFact - | RopeDiffFact - | TickReceiptFact - | RopeCheckpointFact ->; +): FactValidationResult; ``` Validation rules: @@ -493,6 +494,9 @@ Validation rules: - branch children, head roots, leaf blobs, rewrites, diffs, receipts, and checkpoints must reference facts available in the same write set or an already admitted basis; +- reference validation must retrieve typed facts from the current write set or + `admittedBasis.getFact(...)` before checking kind, metrics, content hash, and + worldline linkage; ID existence alone is not a valid reference proof; - the validator receives those scopes through `RopeFactValidationContext` and must not consult ambient process state; - `TextBlobFact.blobId` and `contentHash` must be derived from diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index 21cd4210..fa82aab7 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -136,11 +136,23 @@ test('HT-0149 checkpoint fact carries schema version', () => { test('HT-0149 checkpoint facts are validated rather than deferred', () => { const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); const deferredFacts = sectionBetween(discovery, 'The full design must also define facts for:', 'Echo remains generic.'); + const admittedFacts = sectionBetween(discovery, 'type RopeAdmittedFact =', 'interface RopeFactReadModel'); - assert.match(discovery, /\| TickReceiptFact\n \| RopeCheckpointFact\n>/); + assert.match(admittedFacts, /\| TickReceiptFact\n \| RopeCheckpointFact;/); + assert.match(discovery, /\): FactValidationResult;/); assert.doesNotMatch(deferredFacts, /`RopeCheckpoint`/); }); +test('HT-0149 validation exposes typed admitted facts', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + + assert.match(discovery, /^type RopeAdmittedFact =$/m); + assert.match(discovery, /getFact\(id: string\): RopeAdmittedFact \| null;/); + assert.match(discovery, /\): FactValidationResult;/); + assert.doesNotMatch(discovery, /hasFact\(id: string\): boolean;/); + assert.match(discovery, /reference validation must retrieve typed facts/); +}); + test('HT-0149 specifies concrete rope balance invariants', () => { const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); From 63f0c8320ca20998c7fc1f0be12e92eaab8ff354 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 21:57:24 -0700 Subject: [PATCH 14/16] Fix: require typed rope diff spans --- ...149-graph-backed-rope-runtime-discovery.md | 28 +++++++++++++++---- spec/design-cycle-policy.spec.mjs | 13 +++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index cd9e7a07..d20f402e 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -395,14 +395,28 @@ interface RopeRewriteFact { readonly contentHash: Hash; } -interface RopeDiffSpan { - readonly kind: "equal" | "delete" | "insert"; - readonly basisRange?: TextByteRange; - readonly nextRange?: TextByteRange; - readonly blobId?: TextBlobId; +interface RopeEqualDiffSpan { + readonly kind: "equal"; + readonly basisRange: TextByteRange; + readonly nextRange: TextByteRange; readonly contentHash: Hash; } +interface RopeDeleteDiffSpan { + readonly kind: "delete"; + readonly basisRange: TextByteRange; + readonly contentHash: Hash; +} + +interface RopeInsertDiffSpan { + readonly kind: "insert"; + readonly nextRange: TextByteRange; + readonly blobId: TextBlobId; + readonly contentHash: Hash; +} + +type RopeDiffSpan = RopeEqualDiffSpan | RopeDeleteDiffSpan | RopeInsertDiffSpan; + interface RopeDiffFact { readonly kind: "jedit.text.RopeDiff"; readonly schemaVersion: 1; @@ -511,6 +525,10 @@ Validation rules: metrics before admission; - rewrite, diff, tick receipt, and checkpoint hashes and sequence numbers must be recomputed or range-checked against the admitted basis before admission; +- diff spans are kind-specific: equal spans must carry basis and next ranges, + delete spans must carry only the removed basis range, insert spans must carry + the next range and inserted blob, and validators must reject missing or extra + coordinate fields for each span kind; - invalid facts are rejected before Echo admission and never become retained authority. diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index fa82aab7..a7d99112 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -153,6 +153,19 @@ test('HT-0149 validation exposes typed admitted facts', () => { assert.match(discovery, /reference validation must retrieve typed facts/); }); +test('HT-0149 diff spans are kind-specific', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + + for (const spanName of ['RopeEqualDiffSpan', 'RopeDeleteDiffSpan', 'RopeInsertDiffSpan']) { + assert.match(discovery, new RegExp(`^interface ${spanName} \\{$`, 'm')); + } + + assert.match(discovery, /^type RopeDiffSpan = RopeEqualDiffSpan \| RopeDeleteDiffSpan \| RopeInsertDiffSpan;$/m); + assert.doesNotMatch(discovery, /readonly kind: "equal" \| "delete" \| "insert";/); + assert.doesNotMatch(discovery, /readonly basisRange\?: TextByteRange;/); + assert.match(discovery, /diff spans are kind-specific/); +}); + test('HT-0149 specifies concrete rope balance invariants', () => { const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); From 06067f00da1062f082ac1893bc40ea1180bf00bd Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 22:03:05 -0700 Subject: [PATCH 15/16] Fix: hash tick receipt facts --- docs/design/0149-graph-backed-rope-runtime-discovery.md | 1 + spec/design-cycle-policy.spec.mjs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index d20f402e..a160d55b 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -438,6 +438,7 @@ interface TickReceiptFact { readonly nextHeadId: RopeHeadId; readonly rewriteId: RopeRewriteId; readonly admittedAtSequence: number; + readonly contentHash: Hash; } ``` diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index a7d99112..d605b67b 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -166,6 +166,12 @@ test('HT-0149 diff spans are kind-specific', () => { assert.match(discovery, /diff spans are kind-specific/); }); +test('HT-0149 tick receipt facts carry content hash', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + + assert.match(discovery, /interface TickReceiptFact \{[\s\S]*readonly contentHash: Hash;/); +}); + test('HT-0149 specifies concrete rope balance invariants', () => { const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); From d2adf58443861450b9a8c8a5074c393e41802e62 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 4 Jul 2026 22:10:37 -0700 Subject: [PATCH 16/16] Fix: define rope maintenance facts --- ...149-graph-backed-rope-runtime-discovery.md | 29 +++++++++++++++++++ spec/design-cycle-policy.spec.mjs | 18 +++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/design/0149-graph-backed-rope-runtime-discovery.md b/docs/design/0149-graph-backed-rope-runtime-discovery.md index a160d55b..5b07e4db 100644 --- a/docs/design/0149-graph-backed-rope-runtime-discovery.md +++ b/docs/design/0149-graph-backed-rope-runtime-discovery.md @@ -306,6 +306,8 @@ type TextBlobId = string & { readonly __brand: "TextBlobId" }; type TickId = string & { readonly __brand: "TickId" }; type RopeRewriteId = string & { readonly __brand: "RopeRewriteId" }; type RopeDiffId = string & { readonly __brand: "RopeDiffId" }; +type RopeStructuralMaintenanceId = + string & { readonly __brand: "RopeStructuralMaintenanceId" }; type AdmissionId = string & { readonly __brand: "AdmissionId" }; type Hash = string & { readonly __brand: "Hash" }; @@ -440,6 +442,28 @@ interface TickReceiptFact { readonly admittedAtSequence: number; readonly contentHash: Hash; } + +type RopeStructuralMaintenanceOperation = + | "split-leaf" + | "merge-leaves" + | "rotate-left" + | "rotate-right" + | "rebalance-branch"; + +interface RopeStructuralMaintenanceFact { + readonly kind: "jedit.text.RopeStructuralMaintenance"; + readonly schemaVersion: 1; + readonly maintenanceId: RopeStructuralMaintenanceId; + readonly worldlineId: WorldlineId; + readonly rewriteId: RopeRewriteId; + readonly basisHeadId: RopeHeadId; + readonly nextHeadId: RopeHeadId; + readonly operation: RopeStructuralMaintenanceOperation; + readonly affectedRange: TextByteRange; + readonly replacedNodeIds: readonly RopeNodeId[]; + readonly replacementNodeIds: readonly RopeNodeId[]; + readonly contentHash: Hash; +} ``` The full design must also define facts for: @@ -478,6 +502,7 @@ type RopeAdmittedFact = | RopeRewriteFact | RopeDiffFact | TickReceiptFact + | RopeStructuralMaintenanceFact | RopeCheckpointFact; interface RopeFactReadModel { @@ -530,6 +555,9 @@ Validation rules: delete spans must carry only the removed basis range, insert spans must carry the next range and inserted blob, and validators must reject missing or extra coordinate fields for each span kind; +- structural maintenance facts must reference the semantic rewrite, basis head, + next head, affected range, operation, and exact replaced and replacement node + IDs before an untouched subtree identity exception is accepted; - invalid facts are rejected before Echo admission and never become retained authority. @@ -718,6 +746,7 @@ Durable truth: - `RopeRewrite`; - `RopeDiff`; - `TickReceipt`; +- `RopeStructuralMaintenance`; - `RopeCheckpoint`. Rebuildable indexes and caches: diff --git a/spec/design-cycle-policy.spec.mjs b/spec/design-cycle-policy.spec.mjs index d605b67b..7f84195c 100644 --- a/spec/design-cycle-policy.spec.mjs +++ b/spec/design-cycle-policy.spec.mjs @@ -138,7 +138,8 @@ test('HT-0149 checkpoint facts are validated rather than deferred', () => { const deferredFacts = sectionBetween(discovery, 'The full design must also define facts for:', 'Echo remains generic.'); const admittedFacts = sectionBetween(discovery, 'type RopeAdmittedFact =', 'interface RopeFactReadModel'); - assert.match(admittedFacts, /\| TickReceiptFact\n \| RopeCheckpointFact;/); + assert.match(admittedFacts, /\| TickReceiptFact/); + assert.match(admittedFacts, /\| RopeCheckpointFact;/); assert.match(discovery, /\): FactValidationResult;/); assert.doesNotMatch(deferredFacts, /`RopeCheckpoint`/); }); @@ -172,6 +173,21 @@ test('HT-0149 tick receipt facts carry content hash', () => { assert.match(discovery, /interface TickReceiptFact \{[\s\S]*readonly contentHash: Hash;/); }); +test('HT-0149 defines structural maintenance facts for rebalance exceptions', () => { + const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH); + const admittedFacts = sectionBetween(discovery, 'type RopeAdmittedFact =', 'interface RopeFactReadModel'); + + assert.match(discovery, /^type RopeStructuralMaintenanceId =/m); + assert.match(discovery, /^type RopeStructuralMaintenanceOperation =$/m); + assert.match(discovery, /^interface RopeStructuralMaintenanceFact \{$/m); + assert.match(discovery, /readonly operation: RopeStructuralMaintenanceOperation;/); + assert.match(discovery, /readonly replacedNodeIds: readonly RopeNodeId\[\];/); + assert.match(discovery, /readonly replacementNodeIds: readonly RopeNodeId\[\];/); + assert.match(discovery, /readonly affectedRange: TextByteRange;/); + assert.match(admittedFacts, /\| RopeStructuralMaintenanceFact/); + assert.match(discovery, /structural maintenance facts must reference the semantic rewrite/); +}); + test('HT-0149 specifies concrete rope balance invariants', () => { const discovery = readRepoFile(GRAPH_RUNTIME_DISCOVERY_PATH);