Skip to content

Commit abe4f0a

Browse files
cursoragentjoepio
andcommitted
docs: correct Flutter Loro status and allow flat bind-twin gates
flutter/AGENTS.md still said Loro was missing; Phase A already landed. scripts/consolidation-measure.py --kind bind-twins allows a flat production line count so a shared fixture is not rejected as a no-op. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
1 parent 65e23d8 commit abe4f0a

5 files changed

Lines changed: 78 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain
99

1010
- **Commit ingest is one function.** HTTP `/commit`, hub WS `COMMIT`, Iroh/peer `COMMIT`, and Flutter WS catch-up all call `ingest_commit_json` with a named preset (`hub` / `peer` / `replica`). Replica still skips write-rights and timestamp — the hub already checked, and a replica's ACL graph may be incomplete. WASM `applyCommit` is unchanged (its own `CommitOpts`, signature off).
1111
- **Subscribe auth is one helper.** `Subscribe`, `SubscribeDrive`, and `SubscribeQuery` share `authorize_read` in the commit monitor.
12+
- **Search and server-URL twins share fixtures.** `testdata/search-query.json` binds Rust and TS Tantivy escaping; `testdata/server-url.json` binds the browser and Flutter `normalizeServerUrl`. Empty server-URL input still disagrees (TS `https://`, Dart `''`) — not changed here.
1213

1314
## [v0.41.0-beta.2] - 2026-08-01
1415

flutter/AGENTS.md

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,17 @@ The Kotlin app stores Loro snapshots as local files. The Flutter app uses **atom
3333
- Rust source lives in `/rust/src/`
3434
- `atomic_lib` is referenced as a local path dependency: `../../../atomicdata-dev/atomic-server/lib`
3535

36-
### Loro CRDT: Not Yet Integrated in Flutter
36+
### Loro CRDT: canvas strokes are Loro-backed
3737

38-
The Kotlin app has full Loro CRDT integration (via JNI). The Flutter app currently stores strokes as plain JSON strings to atomic-server. Loro integration is the biggest remaining gap — it needs to be added as a Rust dependency and exposed through `flutter_rust_bridge` for proper offline-first CRDT sync and persistent history.
38+
Phase A of [`canvas-undo-consolidation.md`](../planning/canvas-undo-consolidation.md)
39+
landed. Strokes live in a `LoroList<LoroMap>`; tap-undo / tap-redo go through
40+
`AtomicClient.undoCanvas()` → Rust `resource.undo()` (Loro `UndoManager`);
41+
scrub uses `warmResourceHistory` / version checkout. This is **not** the
42+
biggest remaining gap — do not add a second Loro integration.
43+
44+
Phase B is still open: the Dart `_allActions` stack exists to snapshot
45+
discarded branches and the eraser path. Until that lands, do not delete
46+
`HistoryAction` / `_replayActions`.
3947

4048
### Canvas Coordinate System
4149

@@ -50,7 +58,11 @@ The Kotlin app has full Loro CRDT integration (via JNI). The Flutter app current
5058

5159
### History System
5260

53-
Uses sealed `HistoryAction` classes (`StrokeAdded`, `StrokesDeleted`, `StrokesReplaced`). Undo/redo works by reversing/replaying actions. The Kotlin app also has `DiscardedBranch` — when you undo and then draw, the discarded future is preserved as a branch you can restore. This is partially ported (model exists) but not fully wired up.
61+
Tap-undo / redo and the undo-button scrub gesture are Loro. The sealed
62+
`HistoryAction` classes (`StrokeAdded`, `StrokesDeleted`, `StrokesReplaced`)
63+
and `_allActions` still exist for discarded-branch snapshots (Phase B of
64+
the canvas-undo plan). Do not merge that stack with the browser's
65+
`localStorage` discarded-branches UI — they are twins, not copies.
5466

5567
## File Structure
5668

@@ -83,40 +95,38 @@ lib/
8395
└── src/rust/ # Auto-generated flutter_rust_bridge
8496
```
8597

86-
## What's Done (~35%)
98+
## What's Done
8799

88100
- Core canvas with drawing, pan, zoom
89101
- Stroke rendering with bezier smoothing (CustomPainter)
90102
- Pen tool with color fan (8 hues x 4 shades) and 7 width options
91-
- In-memory undo/redo with action replay
92-
- Gallery with folder organization (local only)
103+
- Loro-backed strokes, tap-undo/redo, and history scrub
104+
- Eraser, gallery thumbnails, pairing / QR, server URL handling
105+
- Gallery with folder organization
93106
- atomic-server integration (agents, drives, canvas CRUD)
94107
- flutter_rust_bridge setup with atomic_lib bindings
95108
- Login/auth screen
96109
- Theme system (Material 3)
97110

98-
## What's Missing (~65%)
111+
## What's Missing
99112

100113
### Critical (must-have for parity)
101-
1. **Loro CRDT integration**add loro crate to Rust, expose through bridge. Needed for offline-first sync, persistent history, and conflict resolution
114+
1. **Phase B canvas undo**drop the Dart action stack; keep discarded branches (see the canvas-undo plan)
102115
2. **Selection + Transform tools** — lasso selection, bounding box handles, scale/rotate/translate strokes
103-
3. **Eraser tool** — stroke deletion by tap/drag
104-
4. **Image import** — background images on canvas
105-
5. **Auto-save** — periodic + on-background save
106-
6. **Thumbnail generation** — PNG thumbnails for gallery (use `dart:ui` Picture recorder)
116+
3. **Image import** — background images on canvas
117+
4. **Auto-save** — periodic + on-background save
107118

108119
### Important (UX parity)
109-
7. **Multi-finger gestures** — 2-finger tap undo, 3-finger tap redo
110-
8. **Stylus hover preview** — show cursor/brush preview on hover
111-
9. **Zoom scrubber** — fine-grained zoom control widget
112-
10. **Fit content** — zoom to fit all strokes with padding
113-
11. **Discarded branches UI** — show/restore abandoned history branches
114-
12. **History persistence** — save/restore undo history across sessions
120+
5. **Multi-finger gestures** — 2-finger tap undo, 3-finger tap redo
121+
6. **Stylus hover preview** — show cursor/brush preview on hover
122+
7. **Zoom scrubber** — fine-grained zoom control widget
123+
8. **Fit content** — zoom to fit all strokes with padding
124+
9. **History persistence** — save/restore undo history across sessions
115125

116126
### Nice-to-have
117-
13. **Folder sync** to atomic-server (currently local-only)
118-
14. **Pressure sensitivity** — vary stroke width by pressure
119-
15. **Tests** — port GeometryTest and CanvasUiTest
127+
10. **Folder sync** to atomic-server (currently local-only)
128+
11. **Pressure sensitivity** — vary stroke width by pressure
129+
12. **Tests** — port GeometryTest and CanvasUiTest
120130

121131
## Reference: Kotlin Source
122132

planning/consolidation-contract.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ and it counts as a public item, so the public-item gate will notice.
125125
```sh
126126
scripts/consolidation-measure.py --baseline /tmp/consol-before.json -- \
127127
lib/src/sync/ws_apply.rs lib/src/sync/engine.rs
128+
# Kind 3 (shared fixtures, no deleted production path):
129+
scripts/consolidation-measure.py --kind bind-twins --baseline /tmp/consol-before.json -- \
130+
lib/src/client/search.rs browser/lib/src/search.ts
128131
```
129132

130133
The script exits non-zero unless the gates below pass.

planning/duplication-and-consolidation.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,11 @@ and Rust `pure_id()` can disagree on the same DID.
158158

159159
Blocks the runtime plan. Concrete copies:
160160

161-
- **Tantivy escape** implemented three times: `browser/lib/src/search.ts`
162-
`escapeTantivyKey`, `lib/src/client/search.rs` `escape_tantivy_key`, and
163-
`server/tests/it/file_search_repro.rs` (test re-implements the browser
164-
helper). `SearchOpts` / `build_search_subject` are also dual.
161+
- **Tantivy escape** in two languages, bound by `testdata/search-query.json`:
162+
`browser/lib/src/search.ts` `escapeTantivyKey` and
163+
`lib/src/client/search.rs` `escape_tantivy_key`. The File-picker repro
164+
reads the escaped `isA` key from that fixture instead of a third copy.
165+
`SearchOpts` / `build_search_subject` are also dual (same fixture).
165166
- **Blob write admission:** HTTP in `server/src/handlers/blob.rs`, WS in
166167
`lib/src/sync/engine.rs`. Same policy, two implementations.
167168
- **Upload File-resource construction** (`save_file_and_create_resource`) and
@@ -210,8 +211,8 @@ the browser unable to sign or speak the wire format without a round-trip.
210211

211212
- Pairing: Flutter should call the same envelope rules as `pairing.ts`
212213
(golden URI fixtures, or parse in Rust and expose via FRB).
213-
- `normalizeServerUrl` / `isLocalAddress`: already documented twins; add
214-
one shared test vector file both suites load.
214+
- `normalizeServerUrl` / `isLocalAddress`: **done**`testdata/server-url.json`.
215+
Empty input still disagrees (TS `https://`, Dart `''`).
215216
- Filter operators: WASM already runs Rust queries locally; client-side
216217
`valueMatches` is the live-membership shortcut. Either call WASM or
217218
share operator fixtures.
@@ -304,11 +305,11 @@ not a third UI — it loads the same SPA.
304305
| Server URL | `helpers/serverUrl.ts` | `atomic/server_url.dart` | Twin, tested separately |
305306
| Documents, tables, chat, AI | data-browser | absent | Expected; Flutter is canvas-first |
306307

307-
`flutter/AGENTS.md` still says Loro is "the biggest remaining gap" and
308-
strokes are stored as JSON. That contradicts
308+
`flutter/AGENTS.md` previously said Loro was "the biggest remaining gap" and
309+
strokes were stored as JSON. That contradicted
309310
[`canvas-undo-consolidation.md`](./canvas-undo-consolidation.md) (Phase A
310-
landed; tap-undo is Loro `UndoManager`). Stale agent context is its own
311-
kind of duplication.
311+
landed; tap-undo is Loro `UndoManager`). **Updated 2026-08-15.** Stale agent
312+
context is its own kind of duplication.
312313

313314
Flutter `flutter/rust/src/api/simple.rs` (~1444 lines) is app-specific
314315
canvas/folder/peer glue. WASM `wasm/src/lib.rs` is closer to a generic
@@ -391,14 +392,16 @@ Every item still has to pass [`consolidation-contract.md`](./consolidation-contr
391392
2. **Delete `browser/lib/src/urls.ts`** — only after data-browser stops using
392393
the nested `urls.properties.*` public API. Not a six-file leftover.
393394
3. **Shared golden fixtures** for pairing URIs, `normalizeServerUrl`,
394-
datatype tags, and Tantivy key escaping.
395+
and Tantivy key escaping — **done (2026-08-15)** (`testdata/search-query.json`,
396+
`testdata/server-url.json`). Datatype tags still open.
395397
4. **`ws_apply::apply_commit_json``ingest_commit_json`****done
396398
(2026-08-15).** `CommitIngestOpts::{hub,peer,replica}`. WASM `applyCommit`
397399
still has its own `CommitOpts`.
398400
5. **Extract `SearchResultsList`**; rename `useDriveHistory`.
399401
6. **One `check_read` helper** in `commit_monitor.rs`**done (2026-08-15)**
400402
(`authorize_read`).
401-
7. **Update `flutter/AGENTS.md`** so it matches the Loro canvas path.
403+
7. **Update `flutter/AGENTS.md`** so it matches the Loro canvas path —
404+
**done (2026-08-15)**.
402405

403406
Then the existing plans, in this order, because each removes a class of
404407
copies rather than one function:

scripts/consolidation-measure.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,16 @@ def render(summary: dict) -> str:
157157
)
158158

159159

160-
def compare(before: dict, after: dict) -> list[str]:
160+
def compare(before: dict, after: dict, kind: str = "delete-duplicate") -> list[str]:
161161
failures = []
162-
if after["production_nonblank"] >= before["production_nonblank"]:
162+
if kind == "bind-twins":
163+
if after["production_nonblank"] > before["production_nonblank"]:
164+
failures.append(
165+
"bind-twins: production non-blank lines grew "
166+
f"({before['production_nonblank']}{after['production_nonblank']}); "
167+
"must not add a third copy"
168+
)
169+
elif after["production_nonblank"] >= before["production_nonblank"]:
163170
failures.append(
164171
"production non-blank lines did not decrease "
165172
f"({before['production_nonblank']}{after['production_nonblank']})"
@@ -221,6 +228,9 @@ def self_test() -> None:
221228
grown = dict(before)
222229
grown["production_nonblank"] = before["production_nonblank"] + 1
223230
assert compare(before, grown), "should fail when lines grow"
231+
232+
assert compare(before, before, kind="bind-twins") == [], "flat ok for bind-twins"
233+
assert compare(before, grown, kind="bind-twins"), "bind-twins must not grow"
224234
print("self-test ok")
225235

226236

@@ -229,6 +239,12 @@ def main(argv: list[str]) -> int:
229239
parser.add_argument("paths", nargs="*", type=Path)
230240
parser.add_argument("--write", type=Path, help="write JSON snapshot")
231241
parser.add_argument("--baseline", type=Path, help="compare against a prior --write")
242+
parser.add_argument(
243+
"--kind",
244+
choices=("delete-duplicate", "policy-split", "bind-twins", "extract"),
245+
default="delete-duplicate",
246+
help="bind-twins allows a flat line count; other kinds require a decrease",
247+
)
232248
parser.add_argument("--json", action="store_true", help="print JSON instead of the table")
233249
parser.add_argument("--self-test", action="store_true")
234250
args = parser.parse_args(argv)
@@ -266,15 +282,21 @@ def main(argv: list[str]) -> int:
266282

267283
if args.baseline:
268284
before = json.loads(args.baseline.read_text())
269-
failures = compare(before, summary)
285+
failures = compare(before, summary, kind=args.kind)
270286
if failures:
271287
print("\nGATE FAILED:", file=sys.stderr)
272288
for failure in failures:
273289
print(f" - {failure}", file=sys.stderr)
274290
return 1
275-
print(
276-
"\ngates passed (production lines down; max file and public items did not grow)"
277-
)
291+
if args.kind == "bind-twins":
292+
print(
293+
"\ngates passed (bind-twins: production lines did not grow; "
294+
"max file and public items did not grow)"
295+
)
296+
else:
297+
print(
298+
"\ngates passed (production lines down; max file and public items did not grow)"
299+
)
278300
return 0
279301

280302

0 commit comments

Comments
 (0)