@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.
- package/README.md +3 -1
- package/apps/studio/acp/index.ts +43 -7
- package/apps/studio/annotations-layer.tsx +191 -116
- package/apps/studio/annotations-model.ts +39 -0
- package/apps/studio/annotations-sync.ts +50 -0
- package/apps/studio/api.ts +732 -38
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-build.ts +49 -8
- package/apps/studio/canvas-edit.ts +468 -7
- package/apps/studio/canvas-lib.tsx +82 -18
- package/apps/studio/canvas-list-watch.ts +33 -2
- package/apps/studio/canvas-notice-message.ts +16 -0
- package/apps/studio/canvas-notifications.tsx +19 -0
- package/apps/studio/canvas-shell.tsx +57 -6
- package/apps/studio/client/app.jsx +783 -245
- package/apps/studio/client/apply-edit-request.ts +55 -0
- package/apps/studio/client/canvas-url.js +39 -1
- package/apps/studio/client/export-center.jsx +111 -61
- package/apps/studio/client/github.js +38 -1
- package/apps/studio/client/index-loader.ts +76 -0
- package/apps/studio/client/panels/CloudBar.jsx +52 -6
- package/apps/studio/client/panels/GitPanel.jsx +103 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
- package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
- package/apps/studio/client/panels/SyncPanel.jsx +113 -0
- package/apps/studio/client/panels/TeamProjects.jsx +376 -0
- package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
- package/apps/studio/client/photo-knobs.jsx +3 -0
- package/apps/studio/client/share-dialog.jsx +91 -0
- package/apps/studio/client/share-link.js +94 -0
- package/apps/studio/client/styles/3-shell-maude.css +36 -18
- package/apps/studio/client/styles/4-components.css +18 -1
- package/apps/studio/client/tree-row-menu.jsx +48 -3
- package/apps/studio/client/whats-new.jsx +26 -32
- package/apps/studio/cloud/endpoints.ts +132 -0
- package/apps/studio/collab/awareness-bridge.ts +65 -14
- package/apps/studio/collab/index.ts +4 -0
- package/apps/studio/collab/persistence.ts +5 -1
- package/apps/studio/collab/registry.ts +9 -3
- package/apps/studio/collab/room.ts +13 -2
- package/apps/studio/context.ts +59 -0
- package/apps/studio/dist/client.bundle.js +1594 -1562
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/sonner.js +1 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/git/accepted-guard.ts +36 -0
- package/apps/studio/hmr-broadcast.ts +34 -1
- package/apps/studio/http.ts +300 -18
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +172 -0
- package/apps/studio/notifications.tsx +292 -0
- package/apps/studio/runtime-bundle.ts +2 -0
- package/apps/studio/server.ts +59 -3
- package/apps/studio/sync/accepted-cold-start.ts +225 -0
- package/apps/studio/sync/accepted-link.ts +320 -0
- package/apps/studio/sync/action-stage.ts +343 -0
- package/apps/studio/sync/agent.ts +12 -60
- package/apps/studio/sync/cell-file-events.ts +3 -0
- package/apps/studio/sync/codec.ts +73 -6
- package/apps/studio/sync/ctl-provider.ts +15 -2
- package/apps/studio/sync/document-discovery.ts +81 -0
- package/apps/studio/sync/file-membership.ts +10 -2
- package/apps/studio/sync/file-plane.ts +309 -26
- package/apps/studio/sync/index.ts +1174 -21
- package/apps/studio/sync/migrate-seed.ts +110 -19
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +315 -2
- package/apps/studio/sync/projection.ts +820 -23
- package/apps/studio/sync/repeated-module.ts +73 -0
- package/apps/studio/sync/revision-barrier.ts +129 -0
- package/apps/studio/sync/seed-repair.ts +46 -0
- package/apps/studio/sync/source-merge.ts +100 -0
- package/apps/studio/sync/source-ops.ts +289 -0
- package/apps/studio/sync/source-recovery.ts +70 -0
- package/apps/studio/sync/source-validation.ts +56 -0
- package/apps/studio/sync/status.ts +83 -1
- package/apps/studio/sync/transaction-client.ts +486 -0
- package/apps/studio/sync/writer-registry.ts +236 -0
- package/apps/studio/text-caret.ts +35 -0
- package/apps/studio/undo-hud.tsx +9 -87
- package/apps/studio/use-canvas-media-drop.tsx +4 -39
- package/apps/studio/use-tool-mode.tsx +44 -0
- package/apps/studio/whats-new.json +137 -0
- package/cli/lib/harness/codex-runtime.mjs +4 -1
- package/package.json +9 -8
- package/plugins/design/dependencies.json +3 -3
- package/plugins/design/templates/_shell.html +95 -7
- 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 =
|
|
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
|
}
|