@1agh/maude 1.2.0 → 1.3.0

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.
Files changed (83) hide show
  1. package/README.md +3 -1
  2. package/apps/studio/acp/index.ts +43 -7
  3. package/apps/studio/annotations-layer.tsx +191 -116
  4. package/apps/studio/annotations-model.ts +39 -0
  5. package/apps/studio/annotations-sync.ts +50 -0
  6. package/apps/studio/api.ts +689 -36
  7. package/apps/studio/bin/annotate.mjs +3 -1
  8. package/apps/studio/bin/server-up.sh +17 -2
  9. package/apps/studio/canvas-edit.ts +391 -7
  10. package/apps/studio/canvas-lib.tsx +82 -18
  11. package/apps/studio/canvas-notice-message.ts +16 -0
  12. package/apps/studio/canvas-notifications.tsx +19 -0
  13. package/apps/studio/canvas-shell.tsx +57 -6
  14. package/apps/studio/client/app.jsx +686 -241
  15. package/apps/studio/client/apply-edit-request.ts +55 -0
  16. package/apps/studio/client/export-center.jsx +111 -61
  17. package/apps/studio/client/github.js +38 -1
  18. package/apps/studio/client/index-loader.ts +76 -0
  19. package/apps/studio/client/panels/CloudBar.jsx +52 -6
  20. package/apps/studio/client/panels/GitPanel.jsx +103 -0
  21. package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
  22. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
  23. package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
  24. package/apps/studio/client/panels/SyncPanel.jsx +113 -0
  25. package/apps/studio/client/panels/TeamProjects.jsx +376 -0
  26. package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
  27. package/apps/studio/client/photo-knobs.jsx +3 -0
  28. package/apps/studio/client/share-dialog.jsx +91 -0
  29. package/apps/studio/client/share-link.js +94 -0
  30. package/apps/studio/client/styles/3-shell-maude.css +35 -18
  31. package/apps/studio/client/styles/4-components.css +18 -1
  32. package/apps/studio/client/tree-row-menu.jsx +48 -3
  33. package/apps/studio/client/whats-new.jsx +26 -32
  34. package/apps/studio/cloud/endpoints.ts +132 -0
  35. package/apps/studio/collab/index.ts +4 -0
  36. package/apps/studio/collab/persistence.ts +5 -1
  37. package/apps/studio/collab/registry.ts +9 -3
  38. package/apps/studio/collab/room.ts +13 -2
  39. package/apps/studio/context.ts +59 -0
  40. package/apps/studio/dist/client.bundle.js +1586 -1554
  41. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  42. package/apps/studio/dist/runtime/sonner.js +1 -0
  43. package/apps/studio/dist/styles.css +1 -1
  44. package/apps/studio/http.ts +289 -18
  45. package/apps/studio/inspect.ts +19 -0
  46. package/apps/studio/managed-projects.ts +150 -0
  47. package/apps/studio/notifications.tsx +292 -0
  48. package/apps/studio/runtime-bundle.ts +2 -0
  49. package/apps/studio/server.ts +59 -3
  50. package/apps/studio/sync/accepted-cold-start.ts +225 -0
  51. package/apps/studio/sync/accepted-link.ts +320 -0
  52. package/apps/studio/sync/action-stage.ts +343 -0
  53. package/apps/studio/sync/agent.ts +12 -60
  54. package/apps/studio/sync/cell-file-events.ts +3 -0
  55. package/apps/studio/sync/codec.ts +73 -6
  56. package/apps/studio/sync/ctl-provider.ts +15 -2
  57. package/apps/studio/sync/document-discovery.ts +81 -0
  58. package/apps/studio/sync/file-membership.ts +10 -2
  59. package/apps/studio/sync/file-plane.ts +309 -26
  60. package/apps/studio/sync/index.ts +1078 -19
  61. package/apps/studio/sync/migrate-seed.ts +110 -19
  62. package/apps/studio/sync/poke.ts +4 -2
  63. package/apps/studio/sync/presentation.ts +285 -2
  64. package/apps/studio/sync/projection.ts +820 -23
  65. package/apps/studio/sync/repeated-module.ts +73 -0
  66. package/apps/studio/sync/revision-barrier.ts +129 -0
  67. package/apps/studio/sync/seed-repair.ts +46 -0
  68. package/apps/studio/sync/source-merge.ts +100 -0
  69. package/apps/studio/sync/source-ops.ts +289 -0
  70. package/apps/studio/sync/source-recovery.ts +70 -0
  71. package/apps/studio/sync/source-validation.ts +56 -0
  72. package/apps/studio/sync/status.ts +79 -1
  73. package/apps/studio/sync/transaction-client.ts +460 -0
  74. package/apps/studio/sync/writer-registry.ts +236 -0
  75. package/apps/studio/text-caret.ts +35 -0
  76. package/apps/studio/undo-hud.tsx +9 -87
  77. package/apps/studio/use-canvas-media-drop.tsx +4 -39
  78. package/apps/studio/whats-new.json +101 -0
  79. package/cli/lib/harness/codex-runtime.mjs +4 -1
  80. package/package.json +9 -8
  81. package/plugins/design/dependencies.json +3 -3
  82. package/plugins/design/templates/_shell.html +47 -0
  83. package/plugins/flow/dependencies.json +3 -3
@@ -0,0 +1,70 @@
1
+ // Bounded recovery slots, outside the rolling snapshot queue: a corrupt peer
2
+ // cannot evict the last valid source by flooding rejected updates (#121).
3
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { atomicWrite } from './atomic-write.ts';
6
+ import { MAX_HTML_BYTES } from './limits.ts';
7
+ import { sourceError } from './source-validation.ts';
8
+
9
+ /**
10
+ * `base` is the body disk and doc last agreed on — the merge base a restart
11
+ * needs, since the journal keeps only its hash (audit 2026-09-13 P0 #1).
12
+ */
13
+ export type RecoverySlot = 'last-valid' | 'local' | 'incoming' | 'base';
14
+
15
+ export function saveRecoveryBody(
16
+ historyDir: string,
17
+ file: string,
18
+ slot: RecoverySlot,
19
+ body: string
20
+ ): string {
21
+ if (Buffer.byteLength(body, 'utf8') > MAX_HTML_BYTES)
22
+ throw new Error('Recovery source exceeds size limit');
23
+ const target = path.join(historyDir, 'sync-recovery', `${slot}${path.extname(file)}`);
24
+ if (!existsSync(target) || readFileSync(target, 'utf8') !== body) atomicWrite(target, body);
25
+ return target;
26
+ }
27
+
28
+ /** One recovery slot's body, or null when absent/unreadable/oversized. */
29
+ export function readRecoveryBody(
30
+ historyDir: string,
31
+ file: string,
32
+ slot: RecoverySlot
33
+ ): string | null {
34
+ const target = path.join(historyDir, 'sync-recovery', `${slot}${path.extname(file)}`);
35
+ try {
36
+ if (statSync(target).size > MAX_HTML_BYTES) return null;
37
+ return readFileSync(target, 'utf8');
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ export function lastValidSource(historyDir: string, file: string): string | null {
44
+ const ext = path.extname(file);
45
+ const retained = path.join(historyDir, 'sync-recovery', `last-valid${ext}`);
46
+ let candidates = [retained];
47
+ try {
48
+ candidates = candidates.concat(
49
+ readdirSync(historyDir, { withFileTypes: true })
50
+ .filter((e) => e.isFile() && e.name.endsWith(ext))
51
+ .map((e) => e.name)
52
+ .sort()
53
+ .reverse()
54
+ .slice(0, 300)
55
+ .map((name) => path.join(historyDir, name))
56
+ );
57
+ } catch {
58
+ /* no older history */
59
+ }
60
+ for (const candidate of candidates) {
61
+ try {
62
+ if (statSync(candidate).size > MAX_HTML_BYTES) continue;
63
+ const body = readFileSync(candidate, 'utf8');
64
+ if (body.trim() && sourceError(file, body) === null) return body;
65
+ } catch {
66
+ /* try the next snapshot */
67
+ }
68
+ }
69
+ return null;
70
+ }
@@ -0,0 +1,56 @@
1
+ // #121: parser success alone does not rule out duplicate source declarations.
2
+ import { parseSync } from 'oxc-parser';
3
+ import { MAX_HTML_BYTES } from './limits.ts';
4
+
5
+ /** Null means safe source syntax; never resolves imports or executes code. */
6
+ export function sourceError(file: string, body: string): string | null {
7
+ if (!/\.[jt]sx?$/i.test(file)) return null;
8
+ if (Buffer.byteLength(body, 'utf8') > MAX_HTML_BYTES) return 'Source exceeds the size limit';
9
+ try {
10
+ const parsed = parseSync(file, body, { sourceType: 'module', showSemanticErrors: true });
11
+ if (parsed.errors.length) return 'Source has syntax errors or duplicate bindings';
12
+ const functions = new Set<string>();
13
+ for (const statement of parsed.program.body) {
14
+ const node =
15
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
16
+ ? statement.declaration
17
+ : statement;
18
+ // TS overload signatures and interface merging are valid. Only multiple
19
+ // implementations collide; OXC currently accepts those even in modules.
20
+ if (node?.type === 'FunctionDeclaration' && node.body && node.id) {
21
+ if (functions.has(node.id.name)) return 'Source has duplicate function implementations';
22
+ functions.add(node.id.name);
23
+ }
24
+ }
25
+ // Overload/type-only declarations may share an exported name. Exclude
26
+ // their own entries, not every later export with that name (#121).
27
+ const typeOnlyExports = new Set(
28
+ parsed.program.body
29
+ .filter(
30
+ (statement) =>
31
+ statement.type === 'ExportNamedDeclaration' &&
32
+ statement.declaration &&
33
+ ['TSDeclareFunction', 'TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(
34
+ statement.declaration.type
35
+ )
36
+ )
37
+ .map((statement) => statement.start)
38
+ );
39
+ const exports = new Set<string>();
40
+ for (const statement of parsed.module.staticExports) {
41
+ if (typeOnlyExports.has(statement.start)) continue;
42
+ for (const entry of statement.entries) {
43
+ if (entry.isType) continue;
44
+ const name = entry.exportName.kind === 'Default' ? 'default' : entry.exportName.name;
45
+ if (name === null) continue;
46
+ if (exports.has(name)) {
47
+ return 'Source has duplicate exports';
48
+ }
49
+ exports.add(name);
50
+ }
51
+ }
52
+ return null;
53
+ } catch {
54
+ return 'Source could not be validated';
55
+ }
56
+ }
@@ -19,7 +19,11 @@ import type { SeedProgress } from './seed-progress.ts';
19
19
  // `cold-start-hub-wins` stays in the union for OLD payload readers (additive
20
20
  // evolution — the NoSyncablePayload discriminator pattern); new conflicts are
21
21
  // recorded as `cold-start-diverged` with the DDR-102 winner + snapshot refs.
22
- export type ConflictKind = 'cold-start-hub-wins' | 'cold-start-diverged' | 'git-pull';
22
+ export type ConflictKind =
23
+ | 'cold-start-hub-wins'
24
+ | 'cold-start-diverged'
25
+ | 'git-pull'
26
+ | 'body-rejected';
23
27
 
24
28
  export interface SyncConflict {
25
29
  slug: string;
@@ -33,6 +37,8 @@ export interface SyncConflict {
33
37
  /** DDR-102 fail-closed (F1) — the local snapshot didn't land, so a hub-wins
34
38
  * overwrite was refused (local kept). Surfaces the degraded `_history/` write. */
35
39
  snapshotFailed?: boolean;
40
+ /** #121 — source refused before materializing or importing. */
41
+ reason?: 'invalid-source' | 'local-edit' | 'history-failed' | 'merge-budget';
36
42
  }
37
43
 
38
44
  export interface SyncStatusPayload extends SyncStatusSnapshot {
@@ -75,6 +81,28 @@ export interface SyncStatusPayload extends SyncStatusSnapshot {
75
81
  * panel and keeps a machine-local dismiss ack keyed on (id, hub url).
76
82
  */
77
83
  notices?: SyncNotice[];
84
+ /**
85
+ * Plan T29 — accepted revisions: what a person is waiting on. Additive;
86
+ * absent until the project saves through accepted revisions.
87
+ */
88
+ accepted?: AcceptedSaveStatus;
89
+ /**
90
+ * Plan T29 — cold open: ms from the runtime's start until every canvas was
91
+ * in step, and until the file plane first converged (media complete).
92
+ * Additive; each appears once it happens.
93
+ */
94
+ coldOpen?: { canvasesMs?: number; filesMs?: number };
95
+ /** Plan T29 — the newest accepted revision this checkout has written (vs the hub's head). */
96
+ appliedRevision?: number;
97
+ /** Plan T16 — an AI action open or held (unfinished). Absent when none. */
98
+ aiAction?: { state: 'open' | 'held'; label: string; canvases: string[]; since: number };
99
+ }
100
+
101
+ export interface AcceptedSaveStatus {
102
+ pending: number;
103
+ oldestPendingAt: number | null;
104
+ ackMs: { last: number | null; p95: number | null; n: number };
105
+ rejected: number;
78
106
  }
79
107
 
80
108
  export interface SyncNotice {
@@ -201,10 +229,18 @@ export interface SyncStatusStore {
201
229
  /** feature-sync-file-plane — merge Plane B counts + persist + broadcast.
202
230
  * Same reasoning as `updateAssets`: a lane, not a connection. */
203
231
  updateFiles(files: FilePlaneStatus): void;
232
+ /** Plan T29 — accepted-revisions save counters. */
233
+ updateAccepted(next: AcceptedSaveStatus): void;
234
+ /** Plan T29 — the newest accepted revision written to this checkout. */
235
+ noteAppliedRevision(revision: number): void;
236
+ /** Plan T16 — the AI action stage (null clears it). */
237
+ updateAiAction(next: SyncStatusPayload['aiAction'] | null): void;
204
238
  /** Record a consent-class notice (A7) + persist + broadcast. Idempotent by
205
239
  * `id` — the notice sites fire once per boot, and a repeat is a no-op
206
240
  * rather than a duplicate row. */
207
241
  notice(notice: Omit<SyncNotice, 'at'>): void;
242
+ /** Remove a resolved source-sync warning; consent notices are untouched. */
243
+ clearSourceConflict(slug: string): void;
208
244
  /** Current payload (defensive copy). */
209
245
  get(): SyncStatusPayload;
210
246
  }
@@ -238,6 +274,11 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
238
274
 
239
275
  let assets: AssetPushProgress | undefined;
240
276
  let files: FilePlaneStatus | undefined;
277
+ let accepted: AcceptedSaveStatus | undefined;
278
+ let aiAction: SyncStatusPayload['aiAction'] | undefined;
279
+ const bootAt = now();
280
+ const coldOpen: { canvasesMs?: number; filesMs?: number } = {};
281
+ let appliedRevision: number | undefined;
241
282
  const notices: SyncNotice[] = [];
242
283
 
243
284
  function payload(): SyncStatusPayload {
@@ -261,6 +302,12 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
261
302
  ...(opts.sharedDoc ? { sharedDoc: true } : {}),
262
303
  ...(assets ? { assets } : {}),
263
304
  ...(files ? { files } : {}),
305
+ ...(accepted ? { accepted } : {}),
306
+ ...(aiAction ? { aiAction } : {}),
307
+ ...(coldOpen.canvasesMs !== undefined || coldOpen.filesMs !== undefined
308
+ ? { coldOpen: { ...coldOpen } }
309
+ : {}),
310
+ ...(appliedRevision !== undefined ? { appliedRevision } : {}),
264
311
  ...(notices.length ? { notices: notices.slice() } : {}),
265
312
  };
266
313
  }
@@ -329,6 +376,10 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
329
376
  update(next) {
330
377
  const changed = snapshot.state !== next.state;
331
378
  snapshot = next;
379
+ const d = next.docs;
380
+ if (coldOpen.canvasesMs === undefined && d && d.synced > 0 && d.pending === 0) {
381
+ coldOpen.canvasesMs = now() - bootAt;
382
+ }
332
383
  // A connection state CHANGE is the headline; a heartbeat is not.
333
384
  flush(changed);
334
385
  },
@@ -341,8 +392,28 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
341
392
  assets = progress;
342
393
  flush();
343
394
  },
395
+ noteAppliedRevision(revision) {
396
+ if (appliedRevision !== undefined && revision <= appliedRevision) return;
397
+ appliedRevision = revision;
398
+ flush();
399
+ },
400
+ updateAiAction(next) {
401
+ aiAction = next ?? undefined;
402
+ flush(true);
403
+ },
404
+ updateAccepted(next) {
405
+ const was = accepted;
406
+ accepted = next;
407
+ // Going from waiting to caught-up (or back) is news; a latency tick is not.
408
+ flush(
409
+ ((was?.pending ?? 0) === 0) !== (next.pending === 0) || next.rejected !== was?.rejected
410
+ );
411
+ },
344
412
  updateFiles(next) {
345
413
  files = next;
414
+ if (coldOpen.filesMs === undefined && next.progress?.phase === 'converged') {
415
+ coldOpen.filesMs = now() - bootAt;
416
+ }
346
417
  // A finished or stalled seed is what a person is waiting to see; a
347
418
  // mid-seed tick is not.
348
419
  flush(next.progress?.phase === 'converged' || next.progress?.phase === 'blocked');
@@ -352,6 +423,13 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
352
423
  notices.push({ ...next, at: now() });
353
424
  flush(true);
354
425
  },
426
+ clearSourceConflict(slug) {
427
+ const id = `source-conflict-${slug}`;
428
+ const index = notices.findIndex((n) => n.id === id);
429
+ if (index < 0) return;
430
+ notices.splice(index, 1);
431
+ flush(true);
432
+ },
355
433
  get: payload,
356
434
  };
357
435
  }