@1agh/maude 1.4.6 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/studio/annotations/ai-read.ts +288 -0
- package/apps/studio/annotations/ai-write.ts +534 -0
- package/apps/studio/annotations/board-io.ts +94 -0
- package/apps/studio/annotations/board-text.ts +45 -0
- package/apps/studio/annotations/constants.ts +58 -0
- package/apps/studio/annotations/elements/_shared.ts +125 -0
- package/apps/studio/annotations/elements/arrow.model.ts +196 -0
- package/apps/studio/annotations/elements/media.model.ts +79 -0
- package/apps/studio/annotations/elements/pen.model.ts +82 -0
- package/apps/studio/annotations/elements/section.model.ts +70 -0
- package/apps/studio/annotations/elements/shape.model.ts +141 -0
- package/apps/studio/annotations/elements/sticky.model.ts +48 -0
- package/apps/studio/annotations/elements/text.model.ts +52 -0
- package/apps/studio/annotations/fields.ts +347 -0
- package/apps/studio/annotations/fractional-index.ts +234 -0
- package/apps/studio/annotations/legacy/mini-dom.ts +207 -0
- package/apps/studio/annotations/migrate-boot.ts +172 -0
- package/apps/studio/annotations/migrate-cli.ts +37 -0
- package/apps/studio/annotations/migrate-v1.ts +383 -0
- package/apps/studio/annotations/ops-merge.ts +15 -0
- package/apps/studio/annotations/ops.ts +499 -0
- package/apps/studio/annotations/registry.ts +159 -0
- package/apps/studio/annotations/replica.ts +264 -0
- package/apps/studio/annotations/scene.ts +235 -0
- package/apps/studio/annotations/schema.ts +188 -0
- package/apps/studio/annotations/types.ts +77 -0
- package/apps/studio/annotations/ui/board.ts +126 -0
- package/apps/studio/annotations/ui/containment.ts +118 -0
- package/apps/studio/annotations/ui/edit-actions.ts +551 -0
- package/apps/studio/annotations/ui/editor-channel.ts +62 -0
- package/apps/studio/annotations/ui/element-node.tsx +993 -0
- package/apps/studio/annotations/ui/pipeline-context.ts +17 -0
- package/apps/studio/annotations/ui/pointer-pipeline.ts +150 -0
- package/apps/studio/annotations/ui/render-model.ts +264 -0
- package/apps/studio/annotations/ui/scene.tsx +63 -0
- package/apps/studio/annotations/ui/text-editor.tsx +420 -0
- package/apps/studio/annotations/ui/text-session.ts +140 -0
- package/apps/studio/annotations/ui/text-style.ts +111 -0
- package/apps/studio/annotations/ui/world.ts +47 -0
- package/apps/studio/annotations/v1-adapter.ts +428 -0
- package/apps/studio/annotations-align.ts +21 -6
- package/apps/studio/annotations-bindings.ts +2 -0
- package/apps/studio/annotations-context-toolbar.tsx +9 -13
- package/apps/studio/annotations-groups.ts +3 -0
- package/apps/studio/annotations-layer.tsx +923 -1925
- package/apps/studio/annotations-model.ts +65 -4
- package/apps/studio/annotations-sync.ts +4 -47
- package/apps/studio/api.ts +249 -69
- package/apps/studio/bin/_import-figma.mjs +22 -12
- package/apps/studio/bin/annotate.mjs +331 -838
- package/apps/studio/bin/annotate.sh +4 -4
- package/apps/studio/bin/perf.sh +21 -7
- package/apps/studio/bin/read-annotations.mjs +184 -666
- package/apps/studio/bin/read-annotations.sh +9 -5
- package/apps/studio/canvas-artifacts.ts +9 -0
- package/apps/studio/canvas-lib.tsx +13 -1
- package/apps/studio/canvas-shell.tsx +6 -0
- package/apps/studio/client/app.jsx +98 -35
- package/apps/studio/client/hmr.mjs +1 -1
- package/apps/studio/client/panels/git-grouping.js +2 -2
- package/apps/studio/client/tree-expansion.js +217 -0
- package/apps/studio/collab/index.ts +49 -5
- package/apps/studio/collab/persistence.ts +31 -10
- package/apps/studio/collab/registry.ts +64 -26
- package/apps/studio/commands/annotation-ops-command.ts +72 -0
- package/apps/studio/cursors-overlay.tsx +158 -4
- package/apps/studio/dist/client.bundle.js +850 -850
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/figma/to-strokes.ts +31 -10
- package/apps/studio/git/endpoints.ts +1 -1
- package/apps/studio/git/service.ts +1 -1
- package/apps/studio/git/watch.ts +1 -1
- package/apps/studio/http.ts +90 -15
- package/apps/studio/server.ts +16 -0
- package/apps/studio/sync/accepted-cold-start.ts +12 -4
- package/apps/studio/sync/agent.ts +8 -4
- package/apps/studio/sync/codec.ts +96 -40
- package/apps/studio/sync/file-membership.ts +12 -2
- package/apps/studio/sync/file-plane.ts +1 -1
- package/apps/studio/sync/index.ts +38 -16
- package/apps/studio/sync/journal-client.ts +5 -0
- package/apps/studio/sync/limits.ts +7 -2
- package/apps/studio/sync/migrate-seed.ts +8 -5
- package/apps/studio/sync/projection.ts +7 -2
- package/apps/studio/sync/remote-docs.ts +34 -0
- package/apps/studio/sync/writer-registry.ts +10 -0
- package/apps/studio/text-caret.ts +11 -2
- package/apps/studio/tree-state.ts +45 -0
- package/apps/studio/undo-stack.ts +2 -2
- package/apps/studio/use-annotation-resize.tsx +48 -23
- package/apps/studio/use-annotation-selection.tsx +9 -2
- package/apps/studio/use-collab.tsx +76 -0
- package/apps/studio/whats-new.json +27 -0
- package/cli/lib/design-link.mjs +5 -1
- package/cli/lib/gitignore-block.mjs +1 -1
- package/cli/lib/gitignore-drift.mjs +2 -1
- package/package.json +9 -8
- package/plugins/design/templates/brief-board.tsx.template +1 -1
- package/apps/studio/annotation-edit-base.ts +0 -36
- package/apps/studio/commands/annotation-strokes-command.ts +0 -137
package/apps/studio/server.ts
CHANGED
|
@@ -18,6 +18,7 @@ import path from 'node:path';
|
|
|
18
18
|
import { createAcp } from './acp/index.ts';
|
|
19
19
|
import { cancelInstall, cancelSignin } from './acp/login-state.ts';
|
|
20
20
|
import { createActivity } from './activity.ts';
|
|
21
|
+
import { migrateAnnotationsV2 } from './annotations/migrate-boot.ts';
|
|
21
22
|
import { ASSET_MAX_VIDEO_BYTES, createApi } from './api.ts';
|
|
22
23
|
import { bootSelfHeal } from './boot-self-heal.ts';
|
|
23
24
|
import { isSandboxArmed } from './canvas-build-sandbox.ts';
|
|
@@ -129,6 +130,13 @@ const api = createApi(ctx, {
|
|
|
129
130
|
collab.registry.syncRoomFromAnnotations(api.fileSlug(file), svg, writeId);
|
|
130
131
|
}
|
|
131
132
|
},
|
|
133
|
+
// Code review H1 — the canvas op path goes to a live room's replica, not to a
|
|
134
|
+
// file its debounced flush hasn't caught up with. Accepted mode keeps the
|
|
135
|
+
// disk + proposeLane path: there the hub kernel merges against the base.
|
|
136
|
+
applyAnnotationOpsLive: (file, ops, actionId) => {
|
|
137
|
+
if (ctx.syncControl?.current?.()?.acceptedMode?.()) return null;
|
|
138
|
+
return collab?.registry.applyOpsToRoom(api.fileSlug(file), ops, actionId) ?? null;
|
|
139
|
+
},
|
|
132
140
|
// feature-file-tree-drag-drop-folders (Task 3) — moveCanvas's collab guard
|
|
133
141
|
// + `_active.json` retarget, bridged the same forward-declared way as the
|
|
134
142
|
// comments/annotations hooks above.
|
|
@@ -182,6 +190,13 @@ ctx.bus.on('canvas-list-update', (change) => {
|
|
|
182
190
|
}
|
|
183
191
|
});
|
|
184
192
|
|
|
193
|
+
// DDR-242 — convert legacy `<slug>.annotations.svg` boards to
|
|
194
|
+
// `.annotations.json` BEFORE any room seeds or sync scan reads them. Skipped in
|
|
195
|
+
// a cloud workspace: the hub owns that checkout (its workspace agent migrates).
|
|
196
|
+
if (process.env.MAUDE_WORKSPACE_MODE !== '1') {
|
|
197
|
+
migrateAnnotationsV2({ designRoot: ctx.paths.designRoot });
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
collab = createCollab(ctx, api);
|
|
186
201
|
const aiActivity = createAiActivity(ctx);
|
|
187
202
|
|
|
@@ -518,6 +533,7 @@ function startCanvasServer(port: number): BunServer {
|
|
|
518
533
|
'/_api/git-user': http.routes['/_api/git-user'],
|
|
519
534
|
'/_api/canvas-meta': http.routes['/_api/canvas-meta'],
|
|
520
535
|
'/_api/annotations': http.routes['/_api/annotations'],
|
|
536
|
+
'/_api/annotations/ops': http.routes['/_api/annotations/ops'],
|
|
521
537
|
// Phase 23 — capped binary image upload (magic-byte sniff + category cap +
|
|
522
538
|
// content-addressed name + traversal guard + no-SVG, in api.saveAsset).
|
|
523
539
|
// Bun matches `routes` BEFORE `fetch`, so the route must be listed here
|
|
@@ -40,7 +40,14 @@
|
|
|
40
40
|
import { existsSync, readFileSync } from 'node:fs';
|
|
41
41
|
import type * as Y from 'yjs';
|
|
42
42
|
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
cssFromDoc,
|
|
45
|
+
htmlFromDoc,
|
|
46
|
+
isEmptyAnnotationsSvg,
|
|
47
|
+
laneValueFromFile,
|
|
48
|
+
readLaneFromDoc,
|
|
49
|
+
readLocalAnnotations,
|
|
50
|
+
} from './codec.ts';
|
|
44
51
|
import { commentKey } from './comment-identity.ts';
|
|
45
52
|
import type { CommentLedger } from './comment-ledger.ts';
|
|
46
53
|
import { hashBytes } from './echo-guard.ts';
|
|
@@ -180,8 +187,9 @@ export async function acceptedColdStart(
|
|
|
180
187
|
const metaText = readText(i.paths.meta);
|
|
181
188
|
const meta = metaText === null ? null : laneValueFromFile('meta', metaText);
|
|
182
189
|
if (meta && meta !== '{}') lanes.meta = meta;
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
// DDR-242 — canonical board text; a legacy sidecar arrives migrated.
|
|
191
|
+
const ann = readLocalAnnotations(i.paths.annotations, readText);
|
|
192
|
+
if (ann && !isEmptyAnnotationsSvg(ann)) lanes.annotations = ann;
|
|
185
193
|
const commentsText = readText(i.paths.comments);
|
|
186
194
|
const comments = commentsText === null ? null : laneValueFromFile('comments', commentsText);
|
|
187
195
|
if (comments) lanes.comments = comments;
|
|
@@ -267,7 +275,7 @@ export async function acceptedColdStart(
|
|
|
267
275
|
// ---- comments / annotations — the accepted value wins (see header)
|
|
268
276
|
for (const lane of ['comments', 'annotations'] as const) {
|
|
269
277
|
const p = lane === 'comments' ? i.paths.comments : i.paths.annotations;
|
|
270
|
-
const text = readText(p);
|
|
278
|
+
const text = lane === 'annotations' ? readLocalAnnotations(p, readText) : readText(p);
|
|
271
279
|
const local = text === null ? null : laneValueFromFile(lane, text);
|
|
272
280
|
const accepted = readLaneFromDoc(i.doc, lane);
|
|
273
281
|
if (local === null || local === accepted) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// `.design/<slug>.html` ←→ Y.Text (Y_SYNC_TYPES.html)
|
|
7
7
|
// `.design/_comments/<slug>.json` ←→ Y.Array (Y_TYPES.comments)
|
|
8
|
-
// `.design/<slug>.annotations.
|
|
8
|
+
// `.design/<slug>.annotations.json`←→ 'annotations2' replica (DDR-242)
|
|
9
9
|
//
|
|
10
10
|
// Provider is INJECTED — the agent doesn't import @hocuspocus/provider. This
|
|
11
11
|
// keeps the orchestration testable with an in-memory pair of Y.Docs (no hub
|
|
@@ -51,10 +51,13 @@ import {
|
|
|
51
51
|
commentsFromDoc,
|
|
52
52
|
cssFromDoc,
|
|
53
53
|
htmlFromDoc,
|
|
54
|
+
importAnnotationsFromDisk,
|
|
54
55
|
isEmptyAnnotationsSvg,
|
|
55
56
|
mergeSharedMetaIntoLocal,
|
|
56
57
|
metaFromDoc,
|
|
57
58
|
movedToFromDoc,
|
|
59
|
+
noteAnnotationsOnDisk,
|
|
60
|
+
readLocalAnnotations,
|
|
58
61
|
repairSharedMeta,
|
|
59
62
|
stampAnnotationsEdit,
|
|
60
63
|
stampBodyEdit,
|
|
@@ -88,7 +91,7 @@ export interface CanvasSyncPaths {
|
|
|
88
91
|
html: string;
|
|
89
92
|
/** Absolute path to <designRoot>/_comments/<slug>.json. */
|
|
90
93
|
comments: string;
|
|
91
|
-
/** Absolute path to <designRoot>/<slug>.annotations.
|
|
94
|
+
/** Absolute path to <designRoot>/<slug>.annotations.json (DDR-242). */
|
|
92
95
|
annotations: string;
|
|
93
96
|
/** Absolute path to the canvas `.meta.json` (sibling of the body). Optional:
|
|
94
97
|
* when set (always, in production wiring), shared meta keys (layout/artboards)
|
|
@@ -368,6 +371,7 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
368
371
|
}
|
|
369
372
|
const hash = hashBytes(value);
|
|
370
373
|
echoGuard.record(paths.annotations, hash);
|
|
374
|
+
noteAnnotationsOnDisk(doc, value);
|
|
371
375
|
writer(paths.annotations, value);
|
|
372
376
|
lastAnnotations = value;
|
|
373
377
|
}
|
|
@@ -438,7 +442,7 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
438
442
|
// wrapper written by saveAnnotations) cold-start-safe on other peers.
|
|
439
443
|
let changed = false;
|
|
440
444
|
doc.transact(() => {
|
|
441
|
-
changed =
|
|
445
|
+
changed = importAnnotationsFromDisk(doc, str, origin, lastAnnotations);
|
|
442
446
|
if (changed) stampAnnotationsEdit(doc, origin);
|
|
443
447
|
}, origin);
|
|
444
448
|
if (changed) lastAnnotations = str;
|
|
@@ -470,7 +474,7 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
470
474
|
if (movedToFromDoc(doc) !== null) return;
|
|
471
475
|
const localHtml = readLocal(paths.html);
|
|
472
476
|
const localComments = readLocal(paths.comments);
|
|
473
|
-
const localAnnotations =
|
|
477
|
+
const localAnnotations = readLocalAnnotations(paths.annotations, readLocal);
|
|
474
478
|
const localMeta = paths.meta ? readLocal(paths.meta) : null;
|
|
475
479
|
const localCss = paths.css ? readLocal(paths.css) : null;
|
|
476
480
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ANNOTATION_WRITE_ID } from '../annotations-sync.ts';
|
|
2
1
|
// Y.Doc ↔ disk codecs for the bidirectional file sync agent (Phase 9 Task 4).
|
|
3
2
|
//
|
|
4
3
|
// The agent shuttles three classes of files between disk and the Y.Doc the
|
|
@@ -6,7 +5,8 @@ import { ANNOTATION_WRITE_ID } from '../annotations-sync.ts';
|
|
|
6
5
|
//
|
|
7
6
|
// `.design/<canvas>.html` -> Y.Text (Y_SYNC_TYPES.html)
|
|
8
7
|
// `.design/_comments/<slug>.json` -> Y.Array (Y_TYPES.comments — Phase 6)
|
|
9
|
-
// `.design/<slug>.annotations.
|
|
8
|
+
// `.design/<slug>.annotations.json` -> Y.Map ('annotations2' — DDR-242; per-element replica,
|
|
9
|
+
// see annotations/replica.ts)
|
|
10
10
|
//
|
|
11
11
|
// v1.1 design decision (plan §"Key insight"): HTML body is treated as opaque
|
|
12
12
|
// Y.Text rather than structured Y.XmlFragment. Round-trip drift would
|
|
@@ -26,6 +26,18 @@ import { hostname } from 'node:os';
|
|
|
26
26
|
import { diffChars } from 'diff';
|
|
27
27
|
import type * as Y from 'yjs';
|
|
28
28
|
|
|
29
|
+
import { annotationsLaneValue, canonicalAnnotations } from '../annotations/board-text.ts';
|
|
30
|
+
import { applyOps, diffToOps } from '../annotations/ops.ts';
|
|
31
|
+
import '../annotations/ops-merge.ts';
|
|
32
|
+
import {
|
|
33
|
+
annotationsOnDiskOf,
|
|
34
|
+
isEmptyBoardText,
|
|
35
|
+
noteAnnotationsOnDisk,
|
|
36
|
+
readReplica,
|
|
37
|
+
replicaBoardText,
|
|
38
|
+
writeReplica,
|
|
39
|
+
} from '../annotations/replica.ts';
|
|
40
|
+
import { parseBoard, serializeBoard } from '../annotations/schema.ts';
|
|
29
41
|
import { Y_TYPES } from '../collab/persistence.ts';
|
|
30
42
|
import { commentKey, dedupeCommentsById } from './comment-identity.ts';
|
|
31
43
|
import {
|
|
@@ -293,30 +305,84 @@ export function applyCommentsToDoc(doc: Y.Doc, next: CommentsSnapshot, origin?:
|
|
|
293
305
|
|
|
294
306
|
/* ---------------------------------------------------------------- annotations */
|
|
295
307
|
|
|
296
|
-
|
|
308
|
+
export { canonicalAnnotations };
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The local board (canonical text) at `jsonPath`, falling back to a not-yet-
|
|
312
|
+
* migrated legacy `.annotations.svg` sibling — so a cold start that races the
|
|
313
|
+
* boot migration still sees the local strokes instead of "no local" (which lets
|
|
314
|
+
* the hub win by default: the DDR-223 eraser shape). null = neither exists.
|
|
315
|
+
*/
|
|
316
|
+
export function readLocalAnnotations(
|
|
317
|
+
jsonPath: string,
|
|
318
|
+
read: (abs: string) => string | null
|
|
319
|
+
): string | null {
|
|
320
|
+
const local = read(jsonPath);
|
|
321
|
+
if (local !== null) return canonicalAnnotations(local) ?? local;
|
|
322
|
+
const legacy = read(jsonPath.replace(/\.json$/, '.svg'));
|
|
323
|
+
return legacy === null ? null : canonicalAnnotations(legacy);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The board held by the doc as canonical text, or null if never populated. */
|
|
297
327
|
export function annotationsFromDoc(doc: Y.Doc): string | null {
|
|
298
|
-
|
|
299
|
-
const svg = map.get('svg');
|
|
300
|
-
return typeof svg === 'string' ? svg : null;
|
|
328
|
+
return replicaBoardText(doc);
|
|
301
329
|
}
|
|
302
330
|
|
|
303
331
|
/**
|
|
304
|
-
* True when an annotations value carries ZERO
|
|
305
|
-
*
|
|
306
|
-
* `strokesToSvg([])` emits — 72 bytes, constant across peers).
|
|
332
|
+
* True when an annotations value carries ZERO elements: null, `''`, an empty
|
|
333
|
+
* board, or the bare legacy wrapper `<svg …></svg>`.
|
|
307
334
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
|
|
314
|
-
|
|
335
|
+
* Load-bearing for cold start (the 2026-08-14 annotations eraser, DDR-223): the
|
|
336
|
+
* legacy wrapper was a non-empty STRING, so every `!== ''` guard let a stale hub
|
|
337
|
+
* wrapper overwrite a peer's real strokes. Emptiness is "zero elements", never a
|
|
338
|
+
* byte heuristic. Live delete-all still materializes; only COLD-START decisions
|
|
339
|
+
* treat this as emptiness.
|
|
340
|
+
*/
|
|
341
|
+
export function isEmptyAnnotationsSvg(text: string | null): boolean {
|
|
342
|
+
return isEmptyBoardText(text);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Make the doc's replica hold `next` (board text or legacy SVG), writing only
|
|
347
|
+
* the elements / fields that differ. `null` / `''` → an empty board. A
|
|
348
|
+
* filesystem import is a new operation, never the previous UI author's echo:
|
|
349
|
+
* `writeReplica` clears '~action' on any change made without an action id.
|
|
350
|
+
*/
|
|
351
|
+
/**
|
|
352
|
+
* A disk change to the board, imported into the doc as the CHANGE it made —
|
|
353
|
+
* the ops from what disk last held to what it holds now — never as a
|
|
354
|
+
* replacement. A projection written a moment before a newer edit, whose file
|
|
355
|
+
* event arrives after it, would otherwise put the older values back (the
|
|
356
|
+
* multiplayer rig's lost toolbar edits and deletes). Without a known base
|
|
357
|
+
* (first sight of the file) it is a full apply. Returns whether the doc changed.
|
|
315
358
|
*/
|
|
316
|
-
export
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
359
|
+
export { noteAnnotationsOnDisk };
|
|
360
|
+
|
|
361
|
+
export function importAnnotationsFromDisk(
|
|
362
|
+
doc: Y.Doc,
|
|
363
|
+
next: string | null,
|
|
364
|
+
origin?: unknown,
|
|
365
|
+
fallbackBase?: string | null
|
|
366
|
+
): boolean {
|
|
367
|
+
const text = next === null ? serializeBoard([]) : canonicalAnnotations(next);
|
|
368
|
+
if (text === null || byteLengthUtf8(text) > MAX_ANNOTATIONS_BYTES) {
|
|
369
|
+
return applyAnnotationsToDoc(doc, next, origin);
|
|
370
|
+
}
|
|
371
|
+
const known = annotationsOnDiskOf(doc);
|
|
372
|
+
const base =
|
|
373
|
+
(known !== undefined ? canonicalAnnotations(known) : null) ??
|
|
374
|
+
(fallbackBase ? canonicalAnnotations(fallbackBase) : null);
|
|
375
|
+
const cur = readReplica(doc);
|
|
376
|
+
noteAnnotationsOnDisk(doc, text);
|
|
377
|
+
if (base === null || cur === null) {
|
|
378
|
+
return applyAnnotationsToDoc(doc, text, origin);
|
|
379
|
+
}
|
|
380
|
+
const toMap = (t: string) => new Map(parseBoard(t).elements.map((e) => [e.id, e]));
|
|
381
|
+
const ops = diffToOps(toMap(base), toMap(text));
|
|
382
|
+
if (!ops.length) return false;
|
|
383
|
+
const r = applyOps(new Map(cur.elements.map((e) => [e.id, e])), ops);
|
|
384
|
+
if (!r.touched.size) return false;
|
|
385
|
+
return writeReplica(doc, [...r.state.values()], origin);
|
|
320
386
|
}
|
|
321
387
|
|
|
322
388
|
export function applyAnnotationsToDoc(doc: Y.Doc, next: string | null, origin?: unknown): boolean {
|
|
@@ -326,21 +392,13 @@ export function applyAnnotationsToDoc(doc: Y.Doc, next: string | null, origin?:
|
|
|
326
392
|
);
|
|
327
393
|
return false;
|
|
328
394
|
}
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
map.delete(ANNOTATION_WRITE_ID);
|
|
337
|
-
if (next === null || next === '') {
|
|
338
|
-
map.delete('svg');
|
|
339
|
-
} else {
|
|
340
|
-
map.set('svg', next);
|
|
341
|
-
}
|
|
342
|
-
}, origin);
|
|
343
|
-
return true;
|
|
395
|
+
const text = next === null ? serializeBoard([]) : canonicalAnnotations(next);
|
|
396
|
+
if (text === null) {
|
|
397
|
+
console.warn('[sync/codec] refusing annotations apply: not a board document');
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (readReplica(doc) !== null && replicaBoardText(doc) === text) return false;
|
|
401
|
+
return writeReplica(doc, parseBoard(text).elements, origin);
|
|
344
402
|
}
|
|
345
403
|
|
|
346
404
|
/* ---------------------------------------------------------------- meta */
|
|
@@ -426,7 +484,8 @@ export function laneValueFromFile(
|
|
|
426
484
|
lane: 'html' | 'css' | 'meta' | 'annotations' | 'comments',
|
|
427
485
|
text: string
|
|
428
486
|
): string | null {
|
|
429
|
-
if (lane === 'html' || lane === 'css'
|
|
487
|
+
if (lane === 'html' || lane === 'css') return text;
|
|
488
|
+
if (lane === 'annotations') return annotationsLaneValue(text);
|
|
430
489
|
let parsed: unknown;
|
|
431
490
|
try {
|
|
432
491
|
parsed = parseJsonSafe(text);
|
|
@@ -448,10 +507,7 @@ export function readLaneFromDoc(
|
|
|
448
507
|
lane: 'html' | 'css' | 'meta' | 'annotations' | 'comments'
|
|
449
508
|
): string {
|
|
450
509
|
if (lane === 'html' || lane === 'css' || lane === 'meta') return doc.getText(lane).toString();
|
|
451
|
-
if (lane === 'annotations')
|
|
452
|
-
const svg = doc.getMap<unknown>(Y_TYPES.annotations).get('svg');
|
|
453
|
-
return typeof svg === 'string' ? svg : '';
|
|
454
|
-
}
|
|
510
|
+
if (lane === 'annotations') return annotationsLaneValue(replicaBoardText(doc) ?? '') ?? '';
|
|
455
511
|
const list = doc.getArray<unknown>(Y_TYPES.comments).toArray();
|
|
456
512
|
return list.length ? JSON.stringify(list) : '';
|
|
457
513
|
}
|
|
@@ -199,6 +199,16 @@ export interface ClassifyOptions {
|
|
|
199
199
|
* `node_modules`, directory segments start alphanumeric, the final segment
|
|
200
200
|
* may start with `_`.
|
|
201
201
|
*/
|
|
202
|
+
/**
|
|
203
|
+
* The annotations board (DDR-242: `<slug>.annotations.json`) and its legacy v1
|
|
204
|
+
* form (`<slug>.annotations.svg`). BOTH stay canvas-owned: the board is the doc
|
|
205
|
+
* lane's, and a stale legacy sidecar must never ride the file plane between
|
|
206
|
+
* machines (the boot migration quarantines it locally).
|
|
207
|
+
*/
|
|
208
|
+
function isAnnotationsSidecar(lowerName: string): boolean {
|
|
209
|
+
return lowerName.endsWith('.annotations.json') || lowerName.endsWith('.annotations.svg');
|
|
210
|
+
}
|
|
211
|
+
|
|
202
212
|
export function classifyProjectFile(rel: string, opts: ClassifyOptions = {}): FileClass {
|
|
203
213
|
const parts = relShape(rel);
|
|
204
214
|
if (parts === null) return 'never';
|
|
@@ -219,7 +229,7 @@ export function classifyProjectFile(rel: string, opts: ClassifyOptions = {}): Fi
|
|
|
219
229
|
// The canvas body and its NAMED sidecars — Plane A's, by construction.
|
|
220
230
|
if (lowerLast.endsWith('.tsx')) return 'canvas-owned';
|
|
221
231
|
if (lowerLast.endsWith('.meta.json')) return 'canvas-owned';
|
|
222
|
-
if (lowerLast
|
|
232
|
+
if (isAnnotationsSidecar(lowerLast)) return 'canvas-owned';
|
|
223
233
|
if (lowerLast.endsWith('.css') && opts.hasFile) {
|
|
224
234
|
const sibling = `${rel.slice(0, -'.css'.length)}.tsx`;
|
|
225
235
|
if (opts.hasFile(sibling)) return 'canvas-owned';
|
|
@@ -238,7 +248,7 @@ export function classifyProjectFile(rel: string, opts: ClassifyOptions = {}): Fi
|
|
|
238
248
|
// pushed over them at 10:50:33). The annotations lane's own stamped
|
|
239
249
|
// newest-wins protection never saw it coming — it guards the DOC lane, and
|
|
240
250
|
// this was the file plane acting alone. One owner: the canvas.
|
|
241
|
-
if (parts.length === 1 && lowerLast
|
|
251
|
+
if (parts.length === 1 && isAnnotationsSidecar(lowerLast)) return 'canvas-owned';
|
|
242
252
|
|
|
243
253
|
if (COMPANION_SIDECAR_SUFFIXES.some((s) => lowerLast.endsWith(s))) return 'companion-text';
|
|
244
254
|
|
|
@@ -2397,7 +2397,7 @@ function holdOversized(out: FilePlaneResult, ledger: FileLedger, file: Oversized
|
|
|
2397
2397
|
function referencedAssetNames(designRoot: string): Set<string> {
|
|
2398
2398
|
const out = new Set<string>();
|
|
2399
2399
|
const RE = /assets\/([A-Za-z0-9._-]+\.[A-Za-z0-9]+)/g;
|
|
2400
|
-
const SCANNED = /\.(?:annotations\.svg|tsx|jsx|css|meta\.json)$/i;
|
|
2400
|
+
const SCANNED = /\.(?:annotations\.(?:svg|json)|tsx|jsx|css|meta\.json)$/i;
|
|
2401
2401
|
const walk = (dir: string, depth: number): void => {
|
|
2402
2402
|
if (depth > MAX_WALK_DEPTH) return;
|
|
2403
2403
|
let entries: Dirent[];
|
|
@@ -33,6 +33,8 @@ import { hostname } from 'node:os';
|
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
import type { Awareness } from 'y-protocols/awareness';
|
|
35
35
|
import * as Y from 'yjs';
|
|
36
|
+
import { migrateAnnotationsV2 } from '../annotations/migrate-boot.ts';
|
|
37
|
+
import { REPLICA_TYPE, replicaBoardText } from '../annotations/replica.ts';
|
|
36
38
|
import { renewHubCredential } from '../cloud/renew.ts';
|
|
37
39
|
import { Y_TYPES } from '../collab/persistence.ts';
|
|
38
40
|
import type { Context, LinkedHub } from '../context.ts';
|
|
@@ -71,13 +73,14 @@ import { createFsReader, type FsReader } from './fs-mirror.ts';
|
|
|
71
73
|
import { type HubDocRow, hubHolds, indexHubDocs } from './hub-listing.ts';
|
|
72
74
|
import { getHubRecord } from './hubs-config.ts';
|
|
73
75
|
import { loadJournal, type SyncJournal } from './journal.ts';
|
|
74
|
-
import { hasLedger, hubCapabilities } from './journal-client.ts';
|
|
76
|
+
import { hasAnnotationsV2, hasLedger, hubCapabilities } from './journal-client.ts';
|
|
75
77
|
import { isLoopbackHost } from './loopback.ts';
|
|
76
78
|
import { migrateFlatFallback } from './migrate-flat-fallback.ts';
|
|
77
79
|
import { migrateSeed } from './migrate-seed.ts';
|
|
78
80
|
import { ORIGINS } from './origins.ts';
|
|
79
81
|
import { createDocProjection, type DocProjection } from './projection.ts';
|
|
80
82
|
import {
|
|
83
|
+
acceptedListing,
|
|
81
84
|
describeRemoteDiff,
|
|
82
85
|
diffRemoteDocs,
|
|
83
86
|
fetchRemoteListing,
|
|
@@ -2070,12 +2073,10 @@ export function createSyncRuntime(
|
|
|
2070
2073
|
// connect before doc.create commits; pulling that empty room would lock
|
|
2071
2074
|
// the receiver onto a lossy slug-derived path before the real path arrives.
|
|
2072
2075
|
// The accepted manifest alone names live canvases, including successors
|
|
2073
|
-
// of retired documents whose transport rows may still linger
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
.map((d) => ({ name: d.doc, bytes: bytesByName.get(d.doc) ?? 1 }));
|
|
2078
|
-
return { ...(listing ?? { tombstones: [] }), documents, tombstones: listing?.tombstones ?? [] };
|
|
2076
|
+
// of retired documents whose transport rows may still linger — and a
|
|
2077
|
+
// legacy tombstone for a name the manifest lists live no longer buries it
|
|
2078
|
+
// (see `acceptedListing`).
|
|
2079
|
+
return { ...(listing ?? {}), ...acceptedListing(listing, manifest.docs) };
|
|
2079
2080
|
}
|
|
2080
2081
|
|
|
2081
2082
|
/**
|
|
@@ -2242,6 +2243,9 @@ export function createSyncRuntime(
|
|
|
2242
2243
|
designRoot: ctx.paths.designRoot,
|
|
2243
2244
|
designRel: ctx.paths.designRel,
|
|
2244
2245
|
});
|
|
2246
|
+
// DDR-242 — idempotent; also covers a runtime cycled after boot (a
|
|
2247
|
+
// project linked from the cloud panel) whose tree gained a v1 board.
|
|
2248
|
+
migrateAnnotationsV2({ designRoot: ctx.paths.designRoot });
|
|
2245
2249
|
}
|
|
2246
2250
|
|
|
2247
2251
|
const scan = opts.canvases ? { canvases: opts.canvases, tsxCount: 0 } : await scanCanvases(ctx);
|
|
@@ -2382,18 +2386,23 @@ export function createSyncRuntime(
|
|
|
2382
2386
|
path.sep,
|
|
2383
2387
|
{ ...pathOpts, realpath: realpathOfDeepestExisting, pathFor: manifestPathFor }
|
|
2384
2388
|
);
|
|
2385
|
-
|
|
2389
|
+
// Named as it will actually happen: a canvas the project deleted is listed
|
|
2390
|
+
// for a tick after its tombstone and is not pulled, so it is not announced.
|
|
2391
|
+
const pullNote = describeRemoteDiff({
|
|
2392
|
+
...remoteDiff,
|
|
2393
|
+
hubOnly: remoteDiff.hubOnly.filter((d) => !tombstoned.has(slugFromDocName(d.name) ?? '')),
|
|
2394
|
+
});
|
|
2386
2395
|
if (pullNote) console.log(`[sync] ${pullNote}`);
|
|
2387
2396
|
/** Descriptor paths for one slug at one body path. The sidecar rules live
|
|
2388
2397
|
* here, once: `.meta.json`/`.css` are SIBLINGS of the body, while
|
|
2389
|
-
* `.annotations.
|
|
2398
|
+
* `.annotations.json` is keyed by the flat slug at the design root — the
|
|
2390
2399
|
* asymmetry `workspace-files.mjs` documents, and which moving the body
|
|
2391
2400
|
* must not quietly change. */
|
|
2392
2401
|
const descriptorFor = (slug: string, bodyAbs: string): CanvasDescriptor => ({
|
|
2393
2402
|
slug,
|
|
2394
2403
|
html: bodyAbs,
|
|
2395
2404
|
comments: path.join(ctx.paths.commentsDir, `${slug}.json`),
|
|
2396
|
-
annotations: path.join(ctx.paths.designRoot, `${slug}.annotations.
|
|
2405
|
+
annotations: path.join(ctx.paths.designRoot, `${slug}.annotations.json`),
|
|
2397
2406
|
meta: bodyAbs.replace(/\.tsx$/i, '.meta.json'),
|
|
2398
2407
|
css: bodyAbs.replace(/\.tsx$/i, '.css'),
|
|
2399
2408
|
});
|
|
@@ -3892,21 +3901,23 @@ export function createSyncRuntime(
|
|
|
3892
3901
|
const agentOrigin = agent.origin;
|
|
3893
3902
|
const slug = canvas.slug;
|
|
3894
3903
|
const provComments = provider.document.getArray(Y_TYPES.comments);
|
|
3895
|
-
|
|
3904
|
+
// DDR-242 — the annotations replica: relay the board, the room
|
|
3905
|
+
// diffs it per element (writeReplica), so only changes cross.
|
|
3906
|
+
const provAnn = provider.document.getMap(REPLICA_TYPE);
|
|
3896
3907
|
const onComments = (_e: unknown, tx: { origin: unknown }) => {
|
|
3897
3908
|
if (tx.origin === agentOrigin) return;
|
|
3898
3909
|
reg.syncRoomFromComments?.(slug, provComments.toArray());
|
|
3899
3910
|
};
|
|
3900
3911
|
const onAnn = (_e: unknown, tx: { origin: unknown }) => {
|
|
3901
3912
|
if (tx.origin === agentOrigin) return;
|
|
3902
|
-
const
|
|
3903
|
-
if (
|
|
3913
|
+
const board = replicaBoardText(provider.document);
|
|
3914
|
+
if (board !== null) reg.syncRoomFromAnnotations?.(slug, board);
|
|
3904
3915
|
};
|
|
3905
3916
|
provComments.observe(onComments);
|
|
3906
|
-
provAnn.
|
|
3917
|
+
provAnn.observeDeep(onAnn);
|
|
3907
3918
|
noteDetach(statusDetaches, canvas.slug, () => {
|
|
3908
3919
|
provComments.unobserve(onComments);
|
|
3909
|
-
provAnn.
|
|
3920
|
+
provAnn.unobserveDeep(onAnn);
|
|
3910
3921
|
});
|
|
3911
3922
|
}
|
|
3912
3923
|
},
|
|
@@ -4646,6 +4657,17 @@ export function createSyncRuntime(
|
|
|
4646
4657
|
void hubCapabilities({ hubUrl: linkedHub.url, signal: fileEventsProbe.signal })
|
|
4647
4658
|
.then((caps) => {
|
|
4648
4659
|
if (stopped) return;
|
|
4660
|
+
// DDR-242 — a hub that predates the annotations-v2 model still keeps
|
|
4661
|
+
// boards as SVG: its workspace checkout and kernel would not carry
|
|
4662
|
+
// this studio's `.annotations.json` edits. Say so loudly; the
|
|
4663
|
+
// annotations themselves stay safe (a v1 hub never writes the v2
|
|
4664
|
+
// replica, and this studio never reads its SVG as authoritative).
|
|
4665
|
+
if (caps !== null && !hasAnnotationsV2(caps)) {
|
|
4666
|
+
console.warn(
|
|
4667
|
+
`[sync] ${linkedHub.url} does not advertise annotations-v2 — update the hub; ` +
|
|
4668
|
+
'whiteboard edits will not reach its checkout until it is'
|
|
4669
|
+
);
|
|
4670
|
+
}
|
|
4649
4671
|
if (!hasLedger(caps)) {
|
|
4650
4672
|
// No journal on this hub ⇒ the legacy client carries the upward
|
|
4651
4673
|
// lane, exactly as the pre-v2 desktop did (Open decision 4).
|
|
@@ -5355,7 +5377,7 @@ async function walk(
|
|
|
5355
5377
|
slug,
|
|
5356
5378
|
html: abs,
|
|
5357
5379
|
comments: path.join(commentsDir, `${slug}.json`),
|
|
5358
|
-
annotations: path.join(designRoot, `${slug}.annotations.
|
|
5380
|
+
annotations: path.join(designRoot, `${slug}.annotations.json`),
|
|
5359
5381
|
// The `.meta.json` sidecar sits next to the body: `Foo.tsx` → `Foo.meta.json`.
|
|
5360
5382
|
meta: abs.replace(/\.(tsx|html)$/i, '.meta.json'),
|
|
5361
5383
|
// The `.css` sibling: `Foo.tsx` → `Foo.css` (absent for inline-CSS canvases).
|
|
@@ -212,6 +212,11 @@ export async function hubCapabilities(opts: {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/** Does this hub keep annotations as the v2 element model (DDR-242)? */
|
|
216
|
+
export function hasAnnotationsV2(capabilities: string[] | null): boolean {
|
|
217
|
+
return Array.isArray(capabilities) && capabilities.includes('annotations-v2');
|
|
218
|
+
}
|
|
219
|
+
|
|
215
220
|
/** Does this hub carry the journal file plane? */
|
|
216
221
|
export function hasLedger(capabilities: string[] | null): boolean {
|
|
217
222
|
return Array.isArray(capabilities) && capabilities.includes('ledger');
|
|
@@ -15,8 +15,13 @@
|
|
|
15
15
|
export const MAX_HTML_BYTES = 4 * 1024 * 1024;
|
|
16
16
|
/** `_comments/<slug>.json`, serialized. */
|
|
17
17
|
export const MAX_COMMENTS_BYTES = 1 * 1024 * 1024;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* `<slug>.annotations.json` (DDR-242). Must equal `MAX_BOARD_BYTES` in
|
|
20
|
+
* annotations/constants.ts — annotations-v2-replica.test.ts pins the pair.
|
|
21
|
+
* Raised from v1's 1 MB: the v1 cap bit at ~3.7k elements, and the v2 board is
|
|
22
|
+
* ~3× more compact, so 4 MB is ≈ 20k typical elements (the element-count cap).
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_ANNOTATIONS_BYTES = 4 * 1024 * 1024;
|
|
20
25
|
/** The shared subset of a canvas `.meta.json`. */
|
|
21
26
|
export const MAX_META_BYTES = 1 * 1024 * 1024;
|
|
22
27
|
/** The canvas's sibling stylesheet. */
|
|
@@ -34,10 +34,12 @@ import path from 'node:path';
|
|
|
34
34
|
|
|
35
35
|
import type * as Y from 'yjs';
|
|
36
36
|
|
|
37
|
+
import { readReplica } from '../annotations/replica.ts';
|
|
37
38
|
import { Y_TYPES } from '../collab/persistence.ts';
|
|
38
39
|
import { atomicWrite } from './atomic-write.ts';
|
|
39
40
|
import {
|
|
40
41
|
annotationsEditAtFromDoc,
|
|
42
|
+
annotationsFromDoc,
|
|
41
43
|
applyAnnotationsToDoc,
|
|
42
44
|
applyCommentsToDoc,
|
|
43
45
|
applyCssToDoc,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
applyMetaToDoc,
|
|
46
48
|
bodyEditAtFromDoc,
|
|
47
49
|
isEmptyAnnotationsSvg,
|
|
50
|
+
readLocalAnnotations,
|
|
48
51
|
stampAnnotationsEdit,
|
|
49
52
|
stampBodyEdit,
|
|
50
53
|
Y_SYNC_TYPES,
|
|
@@ -155,8 +158,9 @@ export function docIsEmpty(doc: Y.Doc): boolean {
|
|
|
155
158
|
if (doc.getText(Y_SYNC_TYPES.css).length > 0) return false;
|
|
156
159
|
if (doc.getText(Y_SYNC_TYPES.meta).length > 0) return false;
|
|
157
160
|
if (doc.getArray(Y_TYPES.comments).length > 0) return false;
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
// DDR-242 — the annotations replica (or a legacy v1 value read through the
|
|
162
|
+
// migration): content means at least one element.
|
|
163
|
+
if ((readReplica(doc)?.elements.length ?? 0) > 0) return false;
|
|
160
164
|
return true;
|
|
161
165
|
}
|
|
162
166
|
|
|
@@ -174,7 +178,7 @@ export async function migrateSeed(opts: MigrateSeedOptions): Promise<MigrateSeed
|
|
|
174
178
|
const localHtml =
|
|
175
179
|
diskHtml !== null && sourceError(paths.html, diskHtml) === null ? diskHtml : null;
|
|
176
180
|
const localComments = readLocal(paths.comments);
|
|
177
|
-
const localAnnotations =
|
|
181
|
+
const localAnnotations = readLocalAnnotations(paths.annotations, readLocal);
|
|
178
182
|
const localMeta = paths.meta ? readLocal(paths.meta) : null;
|
|
179
183
|
const readCss = paths.css ? readLocal(paths.css) : null;
|
|
180
184
|
const localCss = readCss === null ? null : (collapseRepeatedText(readCss)?.unit ?? readCss);
|
|
@@ -355,8 +359,7 @@ export async function migrateSeed(opts: MigrateSeedOptions): Promise<MigrateSeed
|
|
|
355
359
|
// right after this seed ran. Resolve the lane here, before the room
|
|
356
360
|
// materializes: unstamped emptiness never beats content.
|
|
357
361
|
{
|
|
358
|
-
const
|
|
359
|
-
const docAnnotations = typeof docSvg === 'string' ? docSvg : '';
|
|
362
|
+
const docAnnotations = annotationsFromDoc(doc) ?? '';
|
|
360
363
|
const annDecision = decideAnnotationsColdStart({
|
|
361
364
|
local: localAnnotations,
|
|
362
365
|
doc: docAnnotations,
|
|
@@ -37,10 +37,12 @@ import {
|
|
|
37
37
|
applyMetaToDoc,
|
|
38
38
|
cssFromDoc,
|
|
39
39
|
htmlFromDoc,
|
|
40
|
+
importAnnotationsFromDisk,
|
|
40
41
|
laneValueFromFile,
|
|
41
42
|
mergeSharedMetaIntoLocal,
|
|
42
43
|
metaFromDoc,
|
|
43
44
|
movedToFromDoc,
|
|
45
|
+
noteAnnotationsOnDisk,
|
|
44
46
|
readLaneFromDoc,
|
|
45
47
|
stampAnnotationsEdit,
|
|
46
48
|
stampBodyEdit,
|
|
@@ -84,7 +86,7 @@ export interface ProjectionPaths {
|
|
|
84
86
|
html: string;
|
|
85
87
|
/** Absolute path to `_comments/<slug>.json`. */
|
|
86
88
|
comments: string;
|
|
87
|
-
/** Absolute path to `<slug>.annotations.
|
|
89
|
+
/** Absolute path to `<slug>.annotations.json` (DDR-242). */
|
|
88
90
|
annotations: string;
|
|
89
91
|
/** Absolute path to the canvas `.meta.json` sibling (optional). */
|
|
90
92
|
meta?: string;
|
|
@@ -449,6 +451,9 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
449
451
|
* fire must never cost the write that already succeeded.
|
|
450
452
|
*/
|
|
451
453
|
function writeAndAnnounce(path: string, value: string): void {
|
|
454
|
+
// The board disk holds from here on — the base a later file event is
|
|
455
|
+
// imported against (codec importAnnotationsFromDisk).
|
|
456
|
+
if (path === paths.annotations) noteAnnotationsOnDisk(doc, value);
|
|
452
457
|
if (readLocal(path) === value) return;
|
|
453
458
|
writer(path, value);
|
|
454
459
|
try {
|
|
@@ -1159,7 +1164,7 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
1159
1164
|
// survives cold start on other peers (the 2026-08-14 eraser fix).
|
|
1160
1165
|
let changed = false;
|
|
1161
1166
|
doc.transact(() => {
|
|
1162
|
-
changed =
|
|
1167
|
+
changed = importAnnotationsFromDisk(doc, str, importOrigin);
|
|
1163
1168
|
if (changed) stampAnnotationsEdit(doc, importOrigin);
|
|
1164
1169
|
}, importOrigin);
|
|
1165
1170
|
return changed;
|