@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.
- 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 +689 -36
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-edit.ts +391 -7
- package/apps/studio/canvas-lib.tsx +82 -18
- 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 +686 -241
- package/apps/studio/client/apply-edit-request.ts +55 -0
- 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 +35 -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/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 +1586 -1554
- 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/http.ts +289 -18
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +150 -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 +1078 -19
- package/apps/studio/sync/migrate-seed.ts +110 -19
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +285 -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 +79 -1
- package/apps/studio/sync/transaction-client.ts +460 -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/whats-new.json +101 -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 +47 -0
- 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,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
|
}
|