@1agh/maude 1.2.0 → 1.3.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.
Files changed (91) 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 +732 -38
  7. package/apps/studio/bin/annotate.mjs +3 -1
  8. package/apps/studio/bin/server-up.sh +17 -2
  9. package/apps/studio/canvas-build.ts +49 -8
  10. package/apps/studio/canvas-edit.ts +468 -7
  11. package/apps/studio/canvas-lib.tsx +82 -18
  12. package/apps/studio/canvas-list-watch.ts +33 -2
  13. package/apps/studio/canvas-notice-message.ts +16 -0
  14. package/apps/studio/canvas-notifications.tsx +19 -0
  15. package/apps/studio/canvas-shell.tsx +57 -6
  16. package/apps/studio/client/app.jsx +783 -245
  17. package/apps/studio/client/apply-edit-request.ts +55 -0
  18. package/apps/studio/client/canvas-url.js +39 -1
  19. package/apps/studio/client/export-center.jsx +111 -61
  20. package/apps/studio/client/github.js +38 -1
  21. package/apps/studio/client/index-loader.ts +76 -0
  22. package/apps/studio/client/panels/CloudBar.jsx +52 -6
  23. package/apps/studio/client/panels/GitPanel.jsx +103 -0
  24. package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
  25. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
  26. package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
  27. package/apps/studio/client/panels/SyncPanel.jsx +113 -0
  28. package/apps/studio/client/panels/TeamProjects.jsx +376 -0
  29. package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
  30. package/apps/studio/client/photo-knobs.jsx +3 -0
  31. package/apps/studio/client/share-dialog.jsx +91 -0
  32. package/apps/studio/client/share-link.js +94 -0
  33. package/apps/studio/client/styles/3-shell-maude.css +36 -18
  34. package/apps/studio/client/styles/4-components.css +18 -1
  35. package/apps/studio/client/tree-row-menu.jsx +48 -3
  36. package/apps/studio/client/whats-new.jsx +26 -32
  37. package/apps/studio/cloud/endpoints.ts +132 -0
  38. package/apps/studio/collab/awareness-bridge.ts +65 -14
  39. package/apps/studio/collab/index.ts +4 -0
  40. package/apps/studio/collab/persistence.ts +5 -1
  41. package/apps/studio/collab/registry.ts +9 -3
  42. package/apps/studio/collab/room.ts +13 -2
  43. package/apps/studio/context.ts +59 -0
  44. package/apps/studio/dist/client.bundle.js +1594 -1562
  45. package/apps/studio/dist/comment-mount.js +2 -2
  46. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  47. package/apps/studio/dist/runtime/sonner.js +1 -0
  48. package/apps/studio/dist/styles.css +1 -1
  49. package/apps/studio/git/accepted-guard.ts +36 -0
  50. package/apps/studio/hmr-broadcast.ts +34 -1
  51. package/apps/studio/http.ts +300 -18
  52. package/apps/studio/inspect.ts +19 -0
  53. package/apps/studio/managed-projects.ts +172 -0
  54. package/apps/studio/notifications.tsx +292 -0
  55. package/apps/studio/runtime-bundle.ts +2 -0
  56. package/apps/studio/server.ts +59 -3
  57. package/apps/studio/sync/accepted-cold-start.ts +225 -0
  58. package/apps/studio/sync/accepted-link.ts +320 -0
  59. package/apps/studio/sync/action-stage.ts +343 -0
  60. package/apps/studio/sync/agent.ts +12 -60
  61. package/apps/studio/sync/cell-file-events.ts +3 -0
  62. package/apps/studio/sync/codec.ts +73 -6
  63. package/apps/studio/sync/ctl-provider.ts +15 -2
  64. package/apps/studio/sync/document-discovery.ts +81 -0
  65. package/apps/studio/sync/file-membership.ts +10 -2
  66. package/apps/studio/sync/file-plane.ts +309 -26
  67. package/apps/studio/sync/index.ts +1174 -21
  68. package/apps/studio/sync/migrate-seed.ts +110 -19
  69. package/apps/studio/sync/poke.ts +4 -2
  70. package/apps/studio/sync/presentation.ts +315 -2
  71. package/apps/studio/sync/projection.ts +820 -23
  72. package/apps/studio/sync/repeated-module.ts +73 -0
  73. package/apps/studio/sync/revision-barrier.ts +129 -0
  74. package/apps/studio/sync/seed-repair.ts +46 -0
  75. package/apps/studio/sync/source-merge.ts +100 -0
  76. package/apps/studio/sync/source-ops.ts +289 -0
  77. package/apps/studio/sync/source-recovery.ts +70 -0
  78. package/apps/studio/sync/source-validation.ts +56 -0
  79. package/apps/studio/sync/status.ts +83 -1
  80. package/apps/studio/sync/transaction-client.ts +486 -0
  81. package/apps/studio/sync/writer-registry.ts +236 -0
  82. package/apps/studio/text-caret.ts +35 -0
  83. package/apps/studio/undo-hud.tsx +9 -87
  84. package/apps/studio/use-canvas-media-drop.tsx +4 -39
  85. package/apps/studio/use-tool-mode.tsx +44 -0
  86. package/apps/studio/whats-new.json +137 -0
  87. package/cli/lib/harness/codex-runtime.mjs +4 -1
  88. package/package.json +9 -8
  89. package/plugins/design/dependencies.json +3 -3
  90. package/plugins/design/templates/_shell.html +95 -7
  91. 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,30 @@ 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;
106
+ /** The project refuses this sign-in — changes wait for signing in again. */
107
+ credentialRefused?: boolean;
78
108
  }
79
109
 
80
110
  export interface SyncNotice {
@@ -201,10 +231,18 @@ export interface SyncStatusStore {
201
231
  /** feature-sync-file-plane — merge Plane B counts + persist + broadcast.
202
232
  * Same reasoning as `updateAssets`: a lane, not a connection. */
203
233
  updateFiles(files: FilePlaneStatus): void;
234
+ /** Plan T29 — accepted-revisions save counters. */
235
+ updateAccepted(next: AcceptedSaveStatus): void;
236
+ /** Plan T29 — the newest accepted revision written to this checkout. */
237
+ noteAppliedRevision(revision: number): void;
238
+ /** Plan T16 — the AI action stage (null clears it). */
239
+ updateAiAction(next: SyncStatusPayload['aiAction'] | null): void;
204
240
  /** Record a consent-class notice (A7) + persist + broadcast. Idempotent by
205
241
  * `id` — the notice sites fire once per boot, and a repeat is a no-op
206
242
  * rather than a duplicate row. */
207
243
  notice(notice: Omit<SyncNotice, 'at'>): void;
244
+ /** Remove a resolved source-sync warning; consent notices are untouched. */
245
+ clearSourceConflict(slug: string): void;
208
246
  /** Current payload (defensive copy). */
209
247
  get(): SyncStatusPayload;
210
248
  }
@@ -238,6 +276,11 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
238
276
 
239
277
  let assets: AssetPushProgress | undefined;
240
278
  let files: FilePlaneStatus | undefined;
279
+ let accepted: AcceptedSaveStatus | undefined;
280
+ let aiAction: SyncStatusPayload['aiAction'] | undefined;
281
+ const bootAt = now();
282
+ const coldOpen: { canvasesMs?: number; filesMs?: number } = {};
283
+ let appliedRevision: number | undefined;
241
284
  const notices: SyncNotice[] = [];
242
285
 
243
286
  function payload(): SyncStatusPayload {
@@ -261,6 +304,12 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
261
304
  ...(opts.sharedDoc ? { sharedDoc: true } : {}),
262
305
  ...(assets ? { assets } : {}),
263
306
  ...(files ? { files } : {}),
307
+ ...(accepted ? { accepted } : {}),
308
+ ...(aiAction ? { aiAction } : {}),
309
+ ...(coldOpen.canvasesMs !== undefined || coldOpen.filesMs !== undefined
310
+ ? { coldOpen: { ...coldOpen } }
311
+ : {}),
312
+ ...(appliedRevision !== undefined ? { appliedRevision } : {}),
264
313
  ...(notices.length ? { notices: notices.slice() } : {}),
265
314
  };
266
315
  }
@@ -329,6 +378,10 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
329
378
  update(next) {
330
379
  const changed = snapshot.state !== next.state;
331
380
  snapshot = next;
381
+ const d = next.docs;
382
+ if (coldOpen.canvasesMs === undefined && d && d.synced > 0 && d.pending === 0) {
383
+ coldOpen.canvasesMs = now() - bootAt;
384
+ }
332
385
  // A connection state CHANGE is the headline; a heartbeat is not.
333
386
  flush(changed);
334
387
  },
@@ -341,8 +394,30 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
341
394
  assets = progress;
342
395
  flush();
343
396
  },
397
+ noteAppliedRevision(revision) {
398
+ if (appliedRevision !== undefined && revision <= appliedRevision) return;
399
+ appliedRevision = revision;
400
+ flush();
401
+ },
402
+ updateAiAction(next) {
403
+ aiAction = next ?? undefined;
404
+ flush(true);
405
+ },
406
+ updateAccepted(next) {
407
+ const was = accepted;
408
+ accepted = next;
409
+ // Going from waiting to caught-up (or back) is news; a latency tick is not.
410
+ flush(
411
+ ((was?.pending ?? 0) === 0) !== (next.pending === 0) ||
412
+ next.rejected !== was?.rejected ||
413
+ !!next.credentialRefused !== !!was?.credentialRefused
414
+ );
415
+ },
344
416
  updateFiles(next) {
345
417
  files = next;
418
+ if (coldOpen.filesMs === undefined && next.progress?.phase === 'converged') {
419
+ coldOpen.filesMs = now() - bootAt;
420
+ }
346
421
  // A finished or stalled seed is what a person is waiting to see; a
347
422
  // mid-seed tick is not.
348
423
  flush(next.progress?.phase === 'converged' || next.progress?.phase === 'blocked');
@@ -352,6 +427,13 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
352
427
  notices.push({ ...next, at: now() });
353
428
  flush(true);
354
429
  },
430
+ clearSourceConflict(slug) {
431
+ const id = `source-conflict-${slug}`;
432
+ const index = notices.findIndex((n) => n.id === id);
433
+ if (index < 0) return;
434
+ notices.splice(index, 1);
435
+ flush(true);
436
+ },
355
437
  get: payload,
356
438
  };
357
439
  }