@1agh/maude 0.55.0 → 0.56.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 (47) hide show
  1. package/apps/studio/bin/_smart-frames.mjs +187 -20
  2. package/apps/studio/bin/_smart-frames.test.mjs +59 -4
  3. package/apps/studio/bin/_transcribe.mjs +40 -3
  4. package/apps/studio/bin/smoke.sh +7 -1
  5. package/apps/studio/client/app.jsx +31 -3
  6. package/apps/studio/client/panels/CloudBar.jsx +44 -5
  7. package/apps/studio/client/panels/GitPanel.jsx +38 -25
  8. package/apps/studio/client/panels/SettingsPanel.jsx +239 -27
  9. package/apps/studio/client/styles/4-components.css +72 -0
  10. package/apps/studio/cloud/endpoints.ts +56 -3
  11. package/apps/studio/collab/persistence.ts +29 -2
  12. package/apps/studio/config.schema.json +3 -3
  13. package/apps/studio/context.ts +41 -0
  14. package/apps/studio/dist/client.bundle.js +1464 -1464
  15. package/apps/studio/dist/comment-mount.js +2 -2
  16. package/apps/studio/dist/styles.css +1 -1
  17. package/apps/studio/generation/gemma-models.ts +312 -14
  18. package/apps/studio/generation/prefs.ts +7 -2
  19. package/apps/studio/generation/runtime-probe.ts +50 -0
  20. package/apps/studio/generation/whisper-models.ts +124 -0
  21. package/apps/studio/hmr-broadcast.ts +67 -0
  22. package/apps/studio/http.ts +196 -109
  23. package/apps/studio/input-router.tsx +55 -2
  24. package/apps/studio/server.ts +11 -9
  25. package/apps/studio/sync/autocommit.ts +61 -2
  26. package/apps/studio/sync/cell-pairing.ts +174 -0
  27. package/apps/studio/sync/codec.ts +11 -5
  28. package/apps/studio/sync/index.ts +239 -26
  29. package/apps/studio/sync/limits.ts +49 -0
  30. package/apps/studio/sync/loopback.ts +21 -0
  31. package/apps/studio/sync/projection.ts +47 -12
  32. package/apps/studio/sync/supervisor.ts +178 -0
  33. package/apps/studio/test/cloud-endpoints.test.ts +95 -0
  34. package/apps/studio/test/csrf-write-guard.test.ts +19 -2
  35. package/apps/studio/test/gemma-models.test.ts +245 -0
  36. package/apps/studio/test/hmr-broadcast.test.ts +57 -1
  37. package/apps/studio/test/input-router.test.ts +95 -0
  38. package/apps/studio/test/shared-doc-cell-pairing.test.ts +639 -0
  39. package/apps/studio/test/sync-autocommit.test.ts +47 -0
  40. package/apps/studio/test/sync-supervisor.test.ts +212 -0
  41. package/apps/studio/test/trusted-request-host.test.ts +66 -0
  42. package/apps/studio/test/whisper-setup.test.ts +97 -0
  43. package/apps/studio/whats-new.json +27 -0
  44. package/apps/studio/ws.ts +9 -1
  45. package/cli/commands/kg.mjs +9 -2
  46. package/package.json +8 -8
  47. package/plugins/design/dependencies.json +21 -3
@@ -8,6 +8,9 @@ import * as Y from 'yjs';
8
8
 
9
9
  import type { Api } from '../api.ts';
10
10
  import type { Context } from '../context.ts';
11
+ // From the LEAF, never from `sync/codec.ts` — codec imports `Y_TYPES` from this
12
+ // file, so reaching for it here would close a cycle (see sync/limits.ts).
13
+ import { MAX_ANNOTATIONS_BYTES, MAX_COMMENTS_BYTES, withinByteCap } from '../sync/limits.ts';
11
14
  import { ensureStateDir, type RoomCallbacks } from './room.ts';
12
15
 
13
16
  /**
@@ -55,6 +58,26 @@ export interface PersistenceDeps {
55
58
  * skipped when the Y type is empty / unset — the JSON file stays whatever
56
59
  * the prior legacy write produced.
57
60
  */
61
+ /**
62
+ * DDR-064 pre-cutover checklist — cap the doc→disk lane for comments and
63
+ * annotations.
64
+ *
65
+ * The codec's `MAX_*_BYTES` guard the FILE→DOC direction. This is the other
66
+ * one, and until shared-doc it barely mattered: the room's doc was populated
67
+ * only by browsers on this machine. Under a shared doc it is populated by the
68
+ * hub, so an oversized array pushed by a peer (or by a hostile hub — DDR-054's
69
+ * threat model, §2d) would be materialized to this disk unbounded. Same ceiling
70
+ * as the import lane, so a value that could never be imported can never be
71
+ * written either.
72
+ *
73
+ * Refuses the WRITE, not the sync: the doc keeps the value and the peers keep
74
+ * converging. What is withheld is turning somebody else's blob into our disk.
75
+ * Shared with `sync/projection.ts`'s equivalent guard via `sync/limits.ts`.
76
+ */
77
+ function withinCap(slug: string, lane: string, value: string, max: number): boolean {
78
+ return withinByteCap(`collab/${slug}`, lane, Buffer.byteLength(value, 'utf8'), max);
79
+ }
80
+
58
81
  export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
59
82
  const { ctx, api, fileForSlug } = deps;
60
83
  const stateDir = ensureStateDir(ctx.paths.designRoot);
@@ -135,14 +158,18 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
135
158
  const list = arr.toArray() as Parameters<Api['saveCommentsForFile']>[1];
136
159
  if (list.length > 0) seenComments.add(slug);
137
160
  if (list.length > 0 || seenComments.has(slug)) {
138
- await api.saveCommentsForFile(file, list);
161
+ if (withinCap(slug, 'comments', JSON.stringify(list), MAX_COMMENTS_BYTES)) {
162
+ await api.saveCommentsForFile(file, list);
163
+ }
139
164
  }
140
165
 
141
166
  // Annotations — Y.Map.svg → annotations.svg file. Task 5.
142
167
  const map = doc.getMap<unknown>(Y_TYPES.annotations);
143
168
  const svg = map.get('svg');
144
169
  if (typeof svg === 'string' && svg) {
145
- await api.saveAnnotations(file, svg);
170
+ if (withinCap(slug, 'annotations', svg, MAX_ANNOTATIONS_BYTES)) {
171
+ await api.saveAnnotations(file, svg);
172
+ }
146
173
  }
147
174
  }
148
175
 
@@ -259,12 +259,12 @@
259
259
  },
260
260
  "transcription": {
261
261
  "type": "object",
262
- "description": "Speech-to-text / subtitle engine preference (Task 2.6, DDR-164). The engine is an EXPLICIT user choice, never an automatic fallback: a chosen-but-unavailable engine errors clearly and Maude never silently switches to another. `maude design transcribe --provider` overrides this per-call.",
262
+ "description": "Speech-to-text / subtitle engine preference (Task 2.6, DDR-164). The engine is never switched behind your back: a chosen-but-unavailable engine errors clearly, and `auto` — a mode you SELECT — resolves to the best engine this machine is set up for while stating which one it picked. `maude design transcribe --provider` overrides this per-call.",
263
263
  "properties": {
264
264
  "provider": {
265
265
  "type": "string",
266
- "enum": ["whisper", "elevenlabs", "groq"],
267
- "description": "Which engine transcribes when no --provider flag is given. `whisper` = local whisper.cpp (free, offline, no key); `elevenlabs` = ElevenLabs Scribe (cloud, key); `groq` = Groq Whisper (cloud, key). Absent ⇒ whisper.",
266
+ "enum": ["auto", "whisper", "elevenlabs", "groq"],
267
+ "description": "Which engine transcribes when no --provider flag is given. `auto` = use the cloud engine whose key is set (ElevenLabs Scribe first), else local whisper — the UI and the CLI both state what it resolved to; `whisper` = local whisper.cpp (free, offline, no key); `elevenlabs` = ElevenLabs Scribe (cloud, key, audio leaves the machine); `groq` = Groq Whisper (cloud, key). Absent ⇒ whisper.",
268
268
  "default": "whisper"
269
269
  },
270
270
  "model": {
@@ -182,6 +182,22 @@ export interface Context {
182
182
  * gates behavior starting in Phase B.
183
183
  */
184
184
  sharedDoc?: boolean;
185
+ /**
186
+ * The live sync runtime's owner, set by server.ts once it exists. Present so
187
+ * the cloud attach lane can START SYNCING the moment a person links a
188
+ * project, instead of leaving them holding a "restart the studio server"
189
+ * note with no button behind it. Structural type (not the SyncSupervisor
190
+ * import) so context.ts stays free of the sync module graph — tests and the
191
+ * non-serving entry points construct a Context without one.
192
+ */
193
+ syncControl?: {
194
+ restart(linkedHub?: LinkedHub): Promise<{
195
+ syncing: boolean;
196
+ canvases: number;
197
+ reason?: string;
198
+ detail?: string;
199
+ }>;
200
+ };
185
201
  }
186
202
 
187
203
  function resolveRepoRoot(): string {
@@ -364,6 +380,12 @@ export function reloadConfig(ctx: Context): boolean {
364
380
  // (syncTsx gating) drift out of step with the hub the socket is actually
365
381
  // attached to — and a poisoned config must never re-point sync without a
366
382
  // restart (DDR-149 fan-out review).
383
+ //
384
+ // This is a rule about the FILE, and it stays. The one path that may change
385
+ // the live link is `adoptLinkedHub` below: a person pressing Connect in
386
+ // trusted app chrome, whose hub credential this same process just stored.
387
+ // That path cycles the runtime itself (sync/supervisor.ts) instead of hoping
388
+ // a watcher notices.
367
389
  if (JSON.stringify(next.linkedHub) !== JSON.stringify(ctx.cfg.linkedHub)) {
368
390
  console.warn(' warn: linkedHub changed — not hot-reloadable, restart the server to apply.');
369
391
  if (ctx.cfg.linkedHub === undefined) delete next.linkedHub;
@@ -389,3 +411,22 @@ export function reloadConfig(ctx: Context): boolean {
389
411
  ctx.cfg.canvasGroups.find((g) => /system/i.test(g.path))?.path ?? 'system';
390
412
  return true;
391
413
  }
414
+
415
+ /**
416
+ * Adopt a link the person just authorized — the ONE way `cfg.linkedHub` may
417
+ * change on a running server.
418
+ *
419
+ * The value is passed BY VALUE from the attach lane (cloud/endpoints.ts), which
420
+ * exchanged it against the configured cloud address and stored the matching hub
421
+ * credential moments earlier. It is deliberately NOT re-read from
422
+ * `.design/config.json`: that file is committed, shared, and writable by
423
+ * anything else on the machine, and re-reading it here would hand the
424
+ * file-watcher's refused capability (reloadConfig above) back through a side
425
+ * door. What the person consented to is what gets applied.
426
+ *
427
+ * Callers must cycle the sync runtime afterwards — `createSyncSupervisor`
428
+ * (sync/supervisor.ts) does both in one step, and is the only intended caller.
429
+ */
430
+ export function adoptLinkedHub(ctx: Context, linkedHub: LinkedHub): void {
431
+ ctx.cfg.linkedHub = linkedHub;
432
+ }