mockpit 1.3.0 → 2.0.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 (45) hide show
  1. package/README.md +61 -45
  2. package/bin/demoData.js +3 -3
  3. package/bin/mockpit.js +327 -52
  4. package/dist/mcp/server.js +35 -0
  5. package/dist/server/apiViews.d.ts +308 -23
  6. package/dist/server/apiViews.js +60 -19
  7. package/dist/server/app.d.ts +4 -1
  8. package/dist/server/app.js +608 -119
  9. package/dist/server/codeWorker.js +2 -1
  10. package/dist/server/decisions.d.ts +5 -5
  11. package/dist/server/decisions.js +10 -9
  12. package/dist/server/designGuide.d.ts +6 -2
  13. package/dist/server/designGuide.js +77 -43
  14. package/dist/server/embeds.d.ts +1 -0
  15. package/dist/server/embeds.js +30 -4
  16. package/dist/server/feedbackBatch.d.ts +6 -6
  17. package/dist/server/feedbackBatch.js +18 -11
  18. package/dist/server/guidance.d.ts +23 -0
  19. package/dist/server/guidance.js +71 -0
  20. package/dist/server/index.js +10 -1
  21. package/dist/server/mcpHttp.d.ts +3 -0
  22. package/dist/server/mcpHttp.js +32 -0
  23. package/dist/server/mcpSpec.d.ts +2 -1
  24. package/dist/server/mcpSpec.js +74 -24
  25. package/dist/server/postSurfaces.js +3 -0
  26. package/dist/server/run.js +6 -1
  27. package/dist/server/runApi.d.ts +3 -3
  28. package/dist/server/runApi.js +16 -12
  29. package/dist/server/sqlStore.d.ts +10 -3
  30. package/dist/server/sqlStore.js +230 -16
  31. package/dist/server/types.d.ts +46 -17
  32. package/dist/server/types.js +34 -11
  33. package/extensions/mockpit.js +179 -16
  34. package/guide/packs/design-craft.md +35 -0
  35. package/guide/topics/asks.md +60 -56
  36. package/guide/topics/feedback.md +104 -74
  37. package/guide/topics/html.md +5 -11
  38. package/guide/topics/http.md +6 -3
  39. package/guide/topics/knobs.md +2 -2
  40. package/guide/topics/scripts.md +2 -2
  41. package/guide/topics/surfaces.md +28 -10
  42. package/package.json +1 -4
  43. package/skills/launch-video/SKILL.md +1 -1
  44. package/skills/mockpit/SKILL.md +18 -10
  45. package/viewer/dist/index.html +8 -7
@@ -1,4 +1,10 @@
1
- import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, sameScreen, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
1
+ import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, isMockKind, newId, normalizeSurfaceIds, projectFromCwd, AREA_ORDER_SETTING, reservedAgent, sameScreen, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
2
+ import { renameSlugRefs } from "./embeds.js";
3
+ import { RULES_SETTING } from "./guidance.js";
4
+ // The settings keyed `<prefix>:<project>`, which a project rename carries.
5
+ const PROJECT_SETTINGS = ["design", AREA_ORDER_SETTING, RULES_SETTING];
6
+ // An optional free-text label: blank means none.
7
+ const label = (v) => stripNul(v ?? "").trim() || null;
2
8
  function parseJson(raw, fallback) {
3
9
  if (typeof raw !== "string" || !raw)
4
10
  return fallback;
@@ -9,6 +15,35 @@ function parseJson(raw, fallback) {
9
15
  return fallback;
10
16
  }
11
17
  }
18
+ // A stored reply in the current names, or null when it already uses them.
19
+ function contractReply(payload) {
20
+ if (!("mix" in payload) && !("decision" in payload) && !("resolved" in payload))
21
+ return null;
22
+ const { mix, decision, resolved, ...rest } = payload;
23
+ const out = { ...rest };
24
+ if (mix !== undefined)
25
+ out.borrowed = mix;
26
+ const kind = decision?.kind;
27
+ if (kind === "accept" || kind === "revise") {
28
+ out.verdict = { ...decision, kind: kind === "accept" ? "use" : "revise" };
29
+ }
30
+ if (resolved !== undefined)
31
+ out.closed = resolved;
32
+ return out;
33
+ }
34
+ // `surfaces` with every `<mockpit-slot>` tag spelled `<mockpit-embed>`, or null
35
+ // when none has one. Only html is markup; a code or markdown surface may quote
36
+ // the tag as text.
37
+ function slotTagsToEmbed(surfaces) {
38
+ let changed = false;
39
+ const out = surfaces.map((s) => {
40
+ if (s.kind !== "html" || !/<\/?mockpit-slot\b/i.test(s.html))
41
+ return s;
42
+ changed = true;
43
+ return { ...s, html: s.html.replace(/<(\/?)mockpit-slot\b/gi, "<$1mockpit-embed") };
44
+ });
45
+ return changed ? out : null;
46
+ }
12
47
  const POST_COLUMNS = `
13
48
  id TEXT PRIMARY KEY, sessionId TEXT NOT NULL, mockId TEXT NOT NULL, state TEXT,
14
49
  variant TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open', title TEXT NOT NULL,
@@ -69,7 +104,7 @@ export class SqlStore {
69
104
  );
70
105
  CREATE TABLE IF NOT EXISTS mocks (
71
106
  id TEXT PRIMARY KEY, project TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL,
72
- kind TEXT NOT NULL DEFAULT 'component', states TEXT NOT NULL DEFAULT '[]',
107
+ kind TEXT NOT NULL DEFAULT 'sketch', states TEXT NOT NULL DEFAULT '[]',
73
108
  asks TEXT NOT NULL DEFAULT '[]', knobs TEXT NOT NULL DEFAULT '{}', draft TEXT,
74
109
  sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
75
110
  widths TEXT NOT NULL DEFAULT '[]'
@@ -108,7 +143,18 @@ export class SqlStore {
108
143
  key: "TEXT",
109
144
  agentActiveAt: "TEXT",
110
145
  });
111
- this.addMissing("mocks", { widths: "TEXT NOT NULL DEFAULT '[]'", resolvedAt: "TEXT" });
146
+ // The close column's first name.
147
+ const mockCols = this.columns("mocks");
148
+ if (mockCols.has("resolvedAt") && !mockCols.has("closedAt")) {
149
+ this.sql.exec("ALTER TABLE mocks RENAME COLUMN resolvedAt TO closedAt");
150
+ }
151
+ this.addMissing("mocks", {
152
+ widths: "TEXT NOT NULL DEFAULT '[]'",
153
+ closedAt: "TEXT",
154
+ area: "TEXT",
155
+ source: "TEXT",
156
+ situations: "TEXT NOT NULL DEFAULT '{}'",
157
+ });
112
158
  if (legacy)
113
159
  this.migrateLegacy();
114
160
  // After the legacy lift, which rebuilds posts from POST_COLUMNS alone.
@@ -124,8 +170,57 @@ export class SqlStore {
124
170
  if (!pinned) {
125
171
  this.sql.exec("UPDATE posts SET acceptedVersion = version WHERE status = 'accepted'");
126
172
  }
173
+ if (this.settingSync("sketchKind") !== "1")
174
+ this.migrateToSketches();
175
+ if (this.settingSync("contractTerms") !== "1")
176
+ this.migrateToContractTerms();
127
177
  this.createIndexes();
128
178
  }
179
+ // The agent contract renamed what replies and drafts store (mix → borrowed,
180
+ // decision → verdict with accept → use, resolved → closed) and retired drop
181
+ // and the `<mockpit-slot>` embed alias. Stored data is rewritten once, here,
182
+ // so nothing past the store reads an old name: a dropped variant is archived
183
+ // (both were hidden from the choice and restorable), a drop verdict loses its
184
+ // verdict (the variant's archived status says the rest), and every stored
185
+ // version spells its embeds `<mockpit-embed>` so it still renders them.
186
+ migrateToContractTerms() {
187
+ this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL WHERE status = 'dropped'");
188
+ for (const r of this.sql
189
+ .exec("SELECT seq, payload FROM comments WHERE kind = 'reply' AND payload IS NOT NULL")
190
+ .toArray()) {
191
+ const payload = parseJson(r.payload, null);
192
+ const next = payload && contractReply(payload);
193
+ if (next) {
194
+ this.sql.exec("UPDATE comments SET payload = ? WHERE seq = ?", JSON.stringify(next), r.seq);
195
+ }
196
+ }
197
+ for (const r of this.sql
198
+ .exec("SELECT id, draft FROM mocks WHERE draft IS NOT NULL")
199
+ .toArray()) {
200
+ const draft = parseJson(r.draft, null);
201
+ if (!draft || !("mix" in draft))
202
+ continue;
203
+ const { mix, ...rest } = draft;
204
+ this.sql.exec("UPDATE mocks SET draft = ? WHERE id = ?", JSON.stringify({ ...rest, borrowed: mix ?? {} }), r.id);
205
+ }
206
+ for (const r of this.sql
207
+ .exec("SELECT id, surfaces FROM posts WHERE surfaces LIKE '%mockpit-slot%'")
208
+ .toArray()) {
209
+ const surfaces = slotTagsToEmbed(parseJson(r.surfaces, []));
210
+ if (surfaces) {
211
+ this.sql.exec("UPDATE posts SET surfaces = ? WHERE id = ?", JSON.stringify(surfaces), r.id);
212
+ }
213
+ }
214
+ for (const r of this.sql
215
+ .exec("SELECT postId, version, surfaces FROM post_versions WHERE surfaces LIKE '%mockpit-slot%'")
216
+ .toArray()) {
217
+ const surfaces = slotTagsToEmbed(parseJson(r.surfaces, []));
218
+ if (surfaces) {
219
+ this.sql.exec("UPDATE post_versions SET surfaces = ? WHERE postId = ? AND version = ?", JSON.stringify(surfaces), r.postId, r.version);
220
+ }
221
+ }
222
+ this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('contractTerms', '1')");
223
+ }
129
224
  createIndexes() {
130
225
  this.sql.exec(`
131
226
  CREATE UNIQUE INDEX IF NOT EXISTS mockpit_mocks_slug_idx ON mocks (project, slug);
@@ -336,7 +431,7 @@ export class SqlStore {
336
431
  draft = {
337
432
  version: 1,
338
433
  answers: {},
339
- mix: {},
434
+ borrowed: {},
340
435
  tuned: {},
341
436
  comments: [],
342
437
  updatedAt: c.createdAt,
@@ -352,6 +447,33 @@ export class SqlStore {
352
447
  }
353
448
  this.sql.exec("DELETE FROM comments WHERE draft = 1");
354
449
  }
450
+ // A mock no one declared used to be a component. Keep that only where it acts
451
+ // as one, embedded somewhere or a step of a flow; the rest become sketches.
452
+ migrateToSketches() {
453
+ const projectOf = new Map();
454
+ for (const r of this.sql.exec("SELECT id, project FROM mocks").toArray()) {
455
+ projectOf.set(r.id, r.project);
456
+ }
457
+ const used = new Set();
458
+ for (const r of this.sql.exec("SELECT mockId, slots FROM posts").toArray()) {
459
+ const project = projectOf.get(r.mockId);
460
+ for (const e of parseJson(r.slots, []))
461
+ used.add(`${project}\u0000${e.slug}`);
462
+ }
463
+ for (const r of this.sql.exec("SELECT project, steps FROM flows").toArray()) {
464
+ for (const x of parseJson(r.steps, [])) {
465
+ used.add(`${r.project}\u0000${x.mock}`);
466
+ }
467
+ }
468
+ for (const r of this.sql
469
+ .exec("SELECT id, project, slug FROM mocks WHERE kind = 'component'")
470
+ .toArray()) {
471
+ if (used.has(`${r.project}\u0000${r.slug}`))
472
+ continue;
473
+ this.sql.exec("UPDATE mocks SET kind = 'sketch' WHERE id = ?", r.id);
474
+ }
475
+ this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('sketchKind', '1')");
476
+ }
355
477
  settingSync(key) {
356
478
  const rows = this.sql.exec("SELECT value FROM settings WHERE key = ?", key).toArray();
357
479
  return rows.length ? rows[0].value : null;
@@ -412,14 +534,17 @@ export class SqlStore {
412
534
  project: r.project,
413
535
  slug: r.slug,
414
536
  title: r.title,
415
- kind: (r.kind === "page" ? "page" : "component"),
537
+ kind: isMockKind(r.kind) ? r.kind : "sketch",
538
+ area: r.area ?? null,
539
+ source: r.source ?? null,
416
540
  states: parseJson(r.states, []),
541
+ situations: parseJson(r.situations, {}),
417
542
  asks: parseJson(r.asks, []),
418
543
  knobs: parseJson(r.knobs, {}),
419
544
  widths: parseJson(r.widths, []),
420
545
  draft: parseJson(r.draft, null),
421
546
  sessionId: r.sessionId ?? null,
422
- resolvedAt: r.resolvedAt ?? null,
547
+ closedAt: r.closedAt ?? null,
423
548
  createdAt: r.createdAt,
424
549
  updatedAt: r.updatedAt,
425
550
  };
@@ -612,18 +737,21 @@ export class SqlStore {
612
737
  project: stripNul(input.project).trim() || DEFAULT_PROJECT,
613
738
  slug: slugify(stripNul(input.slug)),
614
739
  title: stripNul(input.title)?.trim() || input.slug,
615
- kind: input.kind === "page" ? "page" : "component",
740
+ kind: isMockKind(input.kind) ? input.kind : "sketch",
741
+ area: label(input.area),
742
+ source: label(input.source),
616
743
  states: (input.states ?? []).map((s) => stripNul(s)),
744
+ situations: {},
617
745
  asks: [],
618
746
  knobs: input.knobs ?? {},
619
747
  widths: input.widths ?? [],
620
748
  draft: null,
621
749
  sessionId: input.sessionId ?? null,
622
- resolvedAt: null,
750
+ closedAt: null,
623
751
  createdAt: now,
624
752
  updatedAt: now,
625
753
  };
626
- this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, states, asks, knobs, widths, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, '[]', ?, ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, JSON.stringify(mock.states), JSON.stringify(mock.knobs), JSON.stringify(mock.widths), mock.sessionId, mock.createdAt, mock.updatedAt);
754
+ this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, area, source, states, asks, knobs, widths, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '[]', ?, ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, mock.area, mock.source, JSON.stringify(mock.states), JSON.stringify(mock.knobs), JSON.stringify(mock.widths), mock.sessionId, mock.createdAt, mock.updatedAt);
627
755
  return mock;
628
756
  }
629
757
  async updateMock(id, patch) {
@@ -634,15 +762,18 @@ export class SqlStore {
634
762
  ...mock,
635
763
  ...(patch.title !== undefined && { title: stripNul(patch.title).trim() || mock.title }),
636
764
  ...(patch.kind !== undefined && { kind: patch.kind }),
765
+ ...(patch.area !== undefined && { area: label(patch.area) }),
766
+ ...(patch.source !== undefined && { source: label(patch.source) }),
637
767
  ...(patch.states !== undefined && { states: patch.states.map((s) => stripNul(s)) }),
768
+ ...(patch.situations !== undefined && { situations: patch.situations }),
638
769
  ...(patch.asks !== undefined && { asks: patch.asks }),
639
770
  ...(patch.knobs !== undefined && { knobs: patch.knobs }),
640
771
  ...(patch.widths !== undefined && { widths: patch.widths }),
641
772
  ...(patch.sessionId !== undefined && { sessionId: patch.sessionId }),
642
- ...(patch.resolvedAt !== undefined && { resolvedAt: patch.resolvedAt }),
773
+ ...(patch.closedAt !== undefined && { closedAt: patch.closedAt }),
643
774
  updatedAt: new Date().toISOString(),
644
775
  };
645
- this.sql.exec("UPDATE mocks SET title = ?, kind = ?, states = ?, asks = ?, knobs = ?, widths = ?, sessionId = ?, resolvedAt = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), JSON.stringify(next.widths), next.sessionId, next.resolvedAt, next.updatedAt, id);
776
+ this.sql.exec("UPDATE mocks SET title = ?, kind = ?, area = ?, source = ?, states = ?, situations = ?, asks = ?, knobs = ?, widths = ?, sessionId = ?, closedAt = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, next.area, next.source, JSON.stringify(next.states), JSON.stringify(next.situations), JSON.stringify(next.asks), JSON.stringify(next.knobs), JSON.stringify(next.widths), next.sessionId, next.closedAt, next.updatedAt, id);
646
777
  if (patch.asks !== undefined)
647
778
  this.invalidateAssetRefs();
648
779
  return next;
@@ -665,6 +796,92 @@ export class SqlStore {
665
796
  this.invalidateAssetRefs();
666
797
  return true;
667
798
  }
799
+ async renameMock(id, slug) {
800
+ const to = slugify(stripNul(slug));
801
+ return this.atomic(() => {
802
+ const rows = this.sql.exec("SELECT * FROM mocks WHERE id = ?", id).toArray();
803
+ if (rows.length === 0)
804
+ return null;
805
+ const mock = this.rowToMock(rows[0]);
806
+ const from = mock.slug;
807
+ if (to === from)
808
+ return mock;
809
+ const taken = this.sql
810
+ .exec("SELECT id FROM mocks WHERE project = ? AND slug = ?", mock.project, to)
811
+ .toArray();
812
+ if (taken.length > 0)
813
+ return null;
814
+ const updatedAt = new Date().toISOString();
815
+ this.sql.exec("UPDATE mocks SET slug = ?, updatedAt = ? WHERE id = ?", to, updatedAt, id);
816
+ // History is rewritten too: embeds resolve by slug at render time, so an
817
+ // old version left naming the old slug would show its components missing.
818
+ const rewrite = (raw) => {
819
+ let changed = false;
820
+ const next = parseJson(raw, []).map((x) => {
821
+ if (x.kind !== "html")
822
+ return x;
823
+ const html = renameSlugRefs(x.html, from, to);
824
+ if (html === x.html)
825
+ return x;
826
+ changed = true;
827
+ return { ...x, html };
828
+ });
829
+ return changed ? JSON.stringify(next) : null;
830
+ };
831
+ const posts = this.sql
832
+ .exec("SELECT id, surfaces, slots FROM posts WHERE mockId IN (SELECT id FROM mocks WHERE project = ?)", mock.project)
833
+ .toArray();
834
+ for (const r of posts) {
835
+ const postId = r.id;
836
+ const surfaces = rewrite(r.surfaces);
837
+ if (surfaces)
838
+ this.sql.exec("UPDATE posts SET surfaces = ? WHERE id = ?", surfaces, postId);
839
+ const embeds = parseJson(r.slots, []);
840
+ if (embeds.some((e) => e.slug === from)) {
841
+ this.sql.exec("UPDATE posts SET slots = ? WHERE id = ?", JSON.stringify(embeds.map((e) => (e.slug === from ? { ...e, slug: to } : e))), postId);
842
+ }
843
+ for (const v of this.sql
844
+ .exec("SELECT version, surfaces FROM post_versions WHERE postId = ?", postId)
845
+ .toArray()) {
846
+ const old = rewrite(v.surfaces);
847
+ if (!old)
848
+ continue;
849
+ this.sql.exec("UPDATE post_versions SET surfaces = ? WHERE postId = ? AND version = ?", old, postId, v.version);
850
+ }
851
+ }
852
+ for (const flow of this.flowsOf(mock.project)) {
853
+ if (!flow.steps.some((x) => x.mock === from))
854
+ continue;
855
+ this.writeFlow(flow, flow.steps.map((x) => (x.mock === from ? { ...x, mock: to } : x)));
856
+ }
857
+ return { ...mock, slug: to, updatedAt };
858
+ });
859
+ }
860
+ async renameProject(from, to) {
861
+ const target = stripNul(to).trim();
862
+ return this.atomic(() => {
863
+ const occupied = this.sql
864
+ .exec("SELECT COUNT(*) AS n FROM mocks WHERE project = ?", target)
865
+ .one().n;
866
+ if (occupied > 0)
867
+ return false;
868
+ // A project with no mocks has nothing a leftover flow could walk.
869
+ this.sql.exec("DELETE FROM flows WHERE project = ?", target);
870
+ this.sql.exec("UPDATE mocks SET project = ? WHERE project = ?", target, from);
871
+ this.sql.exec("UPDATE sessions SET project = ? WHERE project = ?", target, from);
872
+ this.sql.exec("UPDATE flows SET project = ? WHERE project = ?", target, from);
873
+ for (const prefix of PROJECT_SETTINGS) {
874
+ const value = this.settingSync(`${prefix}:${from}`);
875
+ if (value === null)
876
+ continue;
877
+ this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", `${prefix}:${target}`, value);
878
+ this.sql.exec("DELETE FROM settings WHERE key = ?", `${prefix}:${from}`);
879
+ if (prefix === "design")
880
+ this.invalidateAssetRefs();
881
+ }
882
+ return true;
883
+ });
884
+ }
668
885
  // --- flows ---
669
886
  flowsOf(project) {
670
887
  return this.sql
@@ -985,8 +1202,8 @@ export class SqlStore {
985
1202
  this.sql.exec(input.keepDraft
986
1203
  ? "UPDATE mocks SET asks = ? WHERE id = ?"
987
1204
  : "UPDATE mocks SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.mockId);
988
- if (input.resolvedAt !== undefined) {
989
- this.sql.exec("UPDATE mocks SET resolvedAt = ?, updatedAt = ? WHERE id = ?", input.resolvedAt, input.resolvedAt, input.mockId);
1205
+ if (input.closedAt !== undefined) {
1206
+ this.sql.exec("UPDATE mocks SET closedAt = ?, updatedAt = ? WHERE id = ?", input.closedAt, input.closedAt, input.mockId);
990
1207
  }
991
1208
  for (const { id, version } of input.accept) {
992
1209
  this.sql.exec("UPDATE posts SET status = 'accepted', acceptedVersion = ? WHERE id = ? AND mockId = ?", version, id, input.mockId);
@@ -994,9 +1211,6 @@ export class SqlStore {
994
1211
  for (const id of input.archive) {
995
1212
  this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL WHERE id = ? AND mockId = ?", id, input.mockId);
996
1213
  }
997
- for (const id of input.drop ?? []) {
998
- this.sql.exec("UPDATE posts SET status = 'dropped', acceptedVersion = NULL WHERE id = ? AND mockId = ?", id, input.mockId);
999
- }
1000
1214
  this.invalidateAssetRefs();
1001
1215
  return comment;
1002
1216
  });
@@ -68,6 +68,7 @@ export interface HtmlSurface {
68
68
  kind: "html";
69
69
  html: string;
70
70
  kits?: string[];
71
+ poster?: string;
71
72
  }
72
73
  export interface MarkdownSurface {
73
74
  kind: "markdown";
@@ -196,9 +197,12 @@ export interface PadValue {
196
197
  y: number;
197
198
  }
198
199
  export type KnobValue = number | boolean | string | PadValue | SpringKnob | EasingKnob;
199
- export type MockKind = "page" | "component";
200
- export type PostStatus = "open" | "accepted" | "archived" | "dropped";
201
- export declare const isLive: (status: PostStatus) => boolean;
200
+ export type MockKind = "page" | "component" | "sketch";
201
+ export declare const isMockKind: (k: unknown) => k is MockKind;
202
+ export type PostStatus = "open" | "accepted" | "archived";
203
+ export type VariantStatus = "open" | "inUse" | "archived";
204
+ export declare const variantStatus: (status: PostStatus) => VariantStatus;
205
+ export declare const isLive: (status: PostStatus | VariantStatus) => boolean;
202
206
  export declare const inUseVersion: (post: Pick<Post, "status" | "version" | "acceptedVersion">) => number;
203
207
  export declare function postAtVersion(post: Post, version: number): Post | null;
204
208
  export declare function versionAt(post: Pick<Post, "version" | "history">, n: number): number;
@@ -208,6 +212,7 @@ export interface AskOption {
208
212
  variant?: string;
209
213
  version?: number;
210
214
  set?: Record<string, KnobValue>;
215
+ recommended?: true;
211
216
  }
212
217
  export type AskAnswer = string | string[];
213
218
  export declare const optionState: (ask: Pick<Ask, "state">, mock: {
@@ -247,9 +252,14 @@ export interface PartComment {
247
252
  anchor?: PartCommentAnchor;
248
253
  reaction?: Reaction;
249
254
  variant?: string;
255
+ version?: number;
256
+ borrow?: {
257
+ from: string;
258
+ part?: string;
259
+ };
250
260
  }
251
- export interface ReplyDecision {
252
- kind: "accept" | "revise" | "drop";
261
+ export interface ReplyVerdict {
262
+ kind: "use" | "revise";
253
263
  state: string | null;
254
264
  variant: string;
255
265
  version?: number;
@@ -257,7 +267,7 @@ export interface ReplyDecision {
257
267
  export interface Draft {
258
268
  version: number;
259
269
  answers: Record<string, AskAnswer>;
260
- mix: Record<string, string>;
270
+ borrowed: Record<string, string>;
261
271
  tuned: Record<string, KnobValue>;
262
272
  comments: PartComment[];
263
273
  others?: Record<string, string>;
@@ -270,7 +280,7 @@ export interface Reply {
270
280
  mockId: string;
271
281
  version: number;
272
282
  answers: Record<string, AskAnswer>;
273
- mix: Record<string, string>;
283
+ borrowed: Record<string, string>;
274
284
  tuned: Record<string, KnobValue>;
275
285
  comments: PartComment[];
276
286
  others?: Record<string, string>;
@@ -278,27 +288,40 @@ export interface Reply {
278
288
  otherImages?: Record<string, string[]>;
279
289
  noteImages?: Record<string, string[]>;
280
290
  text?: string;
281
- decision?: ReplyDecision;
282
- resolved?: {
291
+ verdict?: ReplyVerdict;
292
+ closed?: {
283
293
  dismissed: string[];
284
294
  };
295
+ edited?: MockEdit;
296
+ }
297
+ export interface MockEdit {
298
+ kind?: MockKind;
299
+ area?: string | null;
285
300
  }
301
+ export declare const AREA_ORDER_SETTING = "areaOrder";
286
302
  export interface Mock {
287
303
  id: string;
288
304
  project: string;
289
305
  slug: string;
290
306
  title: string;
291
307
  kind: MockKind;
308
+ area: string | null;
309
+ source: string | null;
292
310
  states: string[];
311
+ situations: Record<string, string[]>;
293
312
  asks: Ask[];
294
313
  knobs: Knobs;
295
314
  widths: number[];
296
315
  draft: Draft | null;
297
316
  sessionId: string | null;
298
- resolvedAt: string | null;
317
+ closedAt: string | null;
299
318
  createdAt: string;
300
319
  updatedAt: string;
301
320
  }
321
+ export declare const NORMAL_SITUATION = "Normal";
322
+ export declare const situationKey: (state: string, situation: string) => string;
323
+ export declare function momentOf(situations: Record<string, string[]>, state: string | null): string | null;
324
+ export declare const momentsOf: (states: string[], situations: Record<string, string[]>) => string[];
302
325
  export interface Embed {
303
326
  slug: string;
304
327
  variant: string;
@@ -506,6 +529,8 @@ export interface CreateMockInput {
506
529
  slug: string;
507
530
  title?: string;
508
531
  kind?: MockKind;
532
+ area?: string | null;
533
+ source?: string | null;
509
534
  states?: string[];
510
535
  knobs?: Knobs;
511
536
  widths?: number[];
@@ -514,12 +539,15 @@ export interface CreateMockInput {
514
539
  export interface UpdateMockInput {
515
540
  title?: string;
516
541
  kind?: MockKind;
542
+ area?: string | null;
543
+ source?: string | null;
517
544
  states?: string[];
545
+ situations?: Record<string, string[]>;
518
546
  asks?: Ask[];
519
547
  knobs?: Knobs;
520
548
  widths?: number[];
521
549
  sessionId?: string | null;
522
- resolvedAt?: string | null;
550
+ closedAt?: string | null;
523
551
  }
524
552
  export interface CreatePostInput {
525
553
  sessionId: string;
@@ -577,8 +605,7 @@ export interface CommitReplyInput {
577
605
  version: number;
578
606
  }[];
579
607
  archive: string[];
580
- drop?: string[];
581
- resolvedAt?: string;
608
+ closedAt?: string;
582
609
  keepDraft?: true;
583
610
  }
584
611
  export interface PostQuery {
@@ -604,6 +631,8 @@ export interface Store {
604
631
  createMock(input: CreateMockInput): Promise<Mock>;
605
632
  updateMock(id: string, patch: UpdateMockInput): Promise<Mock | null>;
606
633
  removeMock(id: string): Promise<boolean>;
634
+ renameMock(id: string, slug: string): Promise<Mock | null>;
635
+ renameProject(from: string, to: string): Promise<boolean>;
607
636
  listFlows(project: string): Promise<Flow[]>;
608
637
  placeFlowStep(project: string, name: string, screen: FlowStep, step: number): Promise<Flow | null>;
609
638
  putDraft(mockId: string, draft: Draft | null): Promise<Mock | null>;
@@ -655,16 +684,16 @@ export declare const htmlSurface: (html: string, kits?: unknown) => HtmlSurface;
655
684
  export declare const askAnswered: (a: Pick<Ask, "answer" | "other" | "note">) => boolean;
656
685
  export declare const openAsks: (mock: Pick<Mock, "asks">) => Ask[];
657
686
  export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states"> & {
658
- resolvedAt?: string | null;
687
+ closedAt?: string | null;
659
688
  }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
660
689
  export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states"> & {
661
- resolvedAt?: string | null;
690
+ closedAt?: string | null;
662
691
  }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
663
692
  export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states"> & {
664
- resolvedAt?: string | null;
693
+ closedAt?: string | null;
665
694
  }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
666
695
  export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft"> & {
667
- resolvedAt?: string | null;
696
+ closedAt?: string | null;
668
697
  }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
669
698
  export declare const BUILTIN_ASK_ID = "variant";
670
699
  export declare function builtinAsk(mock: Pick<Mock, "states">, posts: Pick<Post, "state" | "variant">[], id: string): Ask | undefined;
@@ -9,9 +9,8 @@
9
9
  // build their `kind` enums from it, so a kind can't be added to the model
10
10
  // without the MCP tier advertising it too (the gap that left `json`/`code`
11
11
  // publishable over CLI/REST but invisible to MCP). The per-kind FIELD schemas
12
- // in postSurfaces.ts and mcpSpec.ts are still hand-written; test/mcpSpec.test.ts
13
- // guards that every kind here round-trips through both the MCP schema and the
14
- // validator with its fields, so neither half can silently fall behind.
12
+ // in postSurfaces.ts and mcpSpec.ts are still hand-written: a kind added here
13
+ // needs its fields in both, or one half silently falls behind.
15
14
  export const SURFACE_KINDS = [
16
15
  "html",
17
16
  "diff",
@@ -51,8 +50,10 @@ export function isSurfaceKind(kind) {
51
50
  export function isSandboxedSurfaceKind(kind) {
52
51
  return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
53
52
  }
53
+ export const isMockKind = (k) => k === "page" || k === "component" || k === "sketch";
54
+ export const variantStatus = (status) => status === "accepted" ? "inUse" : status;
54
55
  // Still in the running: what the user can choose between.
55
- export const isLive = (status) => status === "open" || status === "accepted";
56
+ export const isLive = (status) => status === "open" || status === "accepted" || status === "inUse";
56
57
  // The version a variant is resolved at when nothing names one: the version in
57
58
  // use when accepted, else its latest.
58
59
  export const inUseVersion = (post) => post.status === "accepted" ? (post.acceptedVersion ?? post.version) : post.version;
@@ -94,6 +95,25 @@ export const OTHER_ID = "other";
94
95
  // for a reviewer on a phone who would rather not type: like, dislike, or
95
96
  // "more like this". Rides as a part comment, so it travels in the one reply.
96
97
  export const REACTIONS = ["up", "down", "more"];
98
+ // The settings key prefix of a project's area order (`areaOrder:<project>`): a
99
+ // JSON array of area names, the order Home lists them in.
100
+ export const AREA_ORDER_SETTING = "areaOrder";
101
+ // The state's own render, when a state has situations.
102
+ export const NORMAL_SITUATION = "Normal";
103
+ // The state a situation is stored as: what feedback, decisions and export name.
104
+ export const situationKey = (state, situation) => `${state} · ${situation}`;
105
+ // The state a situation belongs to, or the state itself.
106
+ export function momentOf(situations, state) {
107
+ if (state === null)
108
+ return null;
109
+ for (const [moment, names] of Object.entries(situations)) {
110
+ if (names.some((n) => situationKey(moment, n) === state))
111
+ return moment;
112
+ }
113
+ return state;
114
+ }
115
+ // The states that are moments of the mock, not situations of one.
116
+ export const momentsOf = (states, situations) => states.filter((st) => momentOf(situations, st) === st);
97
117
  export const sameScreen = (a, b) => a.mock === b.mock && a.state === b.state;
98
118
  export const HISTORY_LIMIT = 20;
99
119
  // "user" is the reserved trust label for genuine human comments. A session
@@ -190,14 +210,14 @@ export const htmlSurface = (html, kits) => ({
190
210
  // answers it too ("Neither, because…").
191
211
  export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
192
212
  export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a) && !a.dismissed);
193
- // How many built-in "Which one?" asks the viewer shows: one mock-wide when every
213
+ // How many built-in "Which variant?" questions the viewer shows: one mock-wide when every
194
214
  // state needs one and the live names line up, else one per needing state. Same
195
215
  // rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
196
216
  // the question the user actually sees. Answering it archives the losers, which
197
217
  // is what makes it stop counting.
198
218
  export function builtinAskIds(mock, posts, mockId) {
199
- // Resolving closed the choice along with every other open question.
200
- if (mock.resolvedAt)
219
+ // Closing the mock closed the choice along with every other open question.
220
+ if (mock.closedAt)
201
221
  return [];
202
222
  const live = (state) => posts
203
223
  .filter((p) => p.mock === mockId && p.state === state && isLive(p.status))
@@ -236,7 +256,7 @@ export function draftedCount(mock, posts) {
236
256
  };
237
257
  return open.filter(said).length;
238
258
  }
239
- // The viewer's built-in "Which one?" for variants no agent ask binds: `variant`
259
+ // The viewer's built-in "Which variant?" for variants no agent ask binds: `variant`
240
260
  // mock-wide, `variant:<state>` per state. Never stored; synthesized from the
241
261
  // variants so a reply answering it validates, flips and reads like any ask. An
242
262
  // option's id is the variant name, so the viewer needs no id mapping.
@@ -257,7 +277,7 @@ export function builtinAsk(mock, posts, id) {
257
277
  return undefined;
258
278
  return {
259
279
  id,
260
- text: "Which one?",
280
+ text: "Which variant?",
261
281
  scope: state === null ? "mock" : "state",
262
282
  ...(state === null ? {} : { state }),
263
283
  options: names.map((v) => ({ id: v, label: v, variant: v })),
@@ -306,7 +326,7 @@ export function surfacesByteLength(surfaces) {
306
326
  let n = 0;
307
327
  for (const p of surfaces) {
308
328
  if (p.kind === "html")
309
- n += p.html.length;
329
+ n += p.html.length + (p.poster?.length ?? 0);
310
330
  else if (p.kind === "diff") {
311
331
  n += p.patch?.length ?? 0;
312
332
  for (const f of p.files ?? [])
@@ -334,7 +354,8 @@ export function surfacesByteLength(surfaces) {
334
354
  }
335
355
  return n;
336
356
  }
337
- // Collect the asset ids an ordered surfaces list references (image surfaces).
357
+ // Collect the asset ids an ordered surfaces list references (image surfaces
358
+ // and html posters).
338
359
  // Used to keep referenced assets out of eviction's first wave. Assets embedded
339
360
  // by raw URL inside html markup are invisible here — touch-on-serve keeps those
340
361
  // warm instead.
@@ -342,6 +363,8 @@ export function collectAssetIds(surfaces, out) {
342
363
  for (const p of surfaces) {
343
364
  if (p.kind === "image")
344
365
  out.add(p.assetId);
366
+ else if (p.kind === "html" && p.poster)
367
+ out.add(p.poster);
345
368
  }
346
369
  }
347
370
  // The images a draft, a reply or a sent ask carries (all keep the user's