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.
- package/README.md +61 -45
- package/bin/demoData.js +3 -3
- package/bin/mockpit.js +327 -52
- package/dist/mcp/server.js +35 -0
- package/dist/server/apiViews.d.ts +308 -23
- package/dist/server/apiViews.js +60 -19
- package/dist/server/app.d.ts +4 -1
- package/dist/server/app.js +608 -119
- package/dist/server/codeWorker.js +2 -1
- package/dist/server/decisions.d.ts +5 -5
- package/dist/server/decisions.js +10 -9
- package/dist/server/designGuide.d.ts +6 -2
- package/dist/server/designGuide.js +77 -43
- package/dist/server/embeds.d.ts +1 -0
- package/dist/server/embeds.js +30 -4
- package/dist/server/feedbackBatch.d.ts +6 -6
- package/dist/server/feedbackBatch.js +18 -11
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- package/dist/server/index.js +10 -1
- package/dist/server/mcpHttp.d.ts +3 -0
- package/dist/server/mcpHttp.js +32 -0
- package/dist/server/mcpSpec.d.ts +2 -1
- package/dist/server/mcpSpec.js +74 -24
- package/dist/server/postSurfaces.js +3 -0
- package/dist/server/run.js +6 -1
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +16 -12
- package/dist/server/sqlStore.d.ts +10 -3
- package/dist/server/sqlStore.js +230 -16
- package/dist/server/types.d.ts +46 -17
- package/dist/server/types.js +34 -11
- package/extensions/mockpit.js +179 -16
- package/guide/packs/design-craft.md +35 -0
- package/guide/topics/asks.md +60 -56
- package/guide/topics/feedback.md +104 -74
- package/guide/topics/html.md +5 -11
- package/guide/topics/http.md +6 -3
- package/guide/topics/knobs.md +2 -2
- package/guide/topics/scripts.md +2 -2
- package/guide/topics/surfaces.md +28 -10
- package/package.json +1 -4
- package/skills/launch-video/SKILL.md +1 -1
- package/skills/mockpit/SKILL.md +18 -10
- package/viewer/dist/index.html +8 -7
package/dist/server/sqlStore.js
CHANGED
|
@@ -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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 = ?,
|
|
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.
|
|
989
|
-
this.sql.exec("UPDATE mocks SET
|
|
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
|
});
|
package/dist/server/types.d.ts
CHANGED
|
@@ -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
|
|
201
|
-
export
|
|
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
|
|
252
|
-
kind: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/server/types.js
CHANGED
|
@@ -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
|
|
13
|
-
//
|
|
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
|
|
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
|
-
//
|
|
200
|
-
if (mock.
|
|
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
|
|
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
|
|
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
|