mockpit 3.0.0 → 3.1.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 (54) hide show
  1. package/README.md +11 -23
  2. package/bin/initDesign.js +11 -16
  3. package/bin/mockpit.js +242 -52
  4. package/bin/poster.js +64 -7
  5. package/dist/mcp/server.js +48 -6
  6. package/dist/server/apiViews.d.ts +562 -557
  7. package/dist/server/apiViews.js +18 -9
  8. package/dist/server/app.js +723 -206
  9. package/dist/server/cdnInline.js +6 -4
  10. package/dist/server/codeWorker.js +2 -2
  11. package/dist/server/contrast.d.ts +4 -0
  12. package/dist/server/contrast.js +157 -0
  13. package/dist/server/decisionList.d.ts +4 -1
  14. package/dist/server/decisionList.js +43 -5
  15. package/dist/server/designGuide.d.ts +13 -6
  16. package/dist/server/designGuide.js +206 -41
  17. package/dist/server/events.d.ts +6 -0
  18. package/dist/server/feedbackBatch.d.ts +4 -0
  19. package/dist/server/feedbackBatch.js +19 -1
  20. package/dist/server/guidance.d.ts +1 -1
  21. package/dist/server/guidance.js +2 -2
  22. package/dist/server/kits.d.ts +2 -2
  23. package/dist/server/kits.js +54 -1
  24. package/dist/server/mcpHttp.d.ts +2 -0
  25. package/dist/server/mcpHttp.js +13 -1
  26. package/dist/server/mcpSpec.d.ts +27 -27
  27. package/dist/server/mcpSpec.js +78 -10
  28. package/dist/server/run.js +22 -1
  29. package/dist/server/runApi.d.ts +2 -2
  30. package/dist/server/runApi.js +9 -2
  31. package/dist/server/sqlStore.d.ts +68 -39
  32. package/dist/server/sqlStore.js +163 -26
  33. package/dist/server/surfacePage.d.ts +4 -2
  34. package/dist/server/surfacePage.js +80 -32
  35. package/dist/server/system.d.ts +234 -0
  36. package/dist/server/system.js +1508 -0
  37. package/dist/server/systemChecks.d.ts +15 -0
  38. package/dist/server/systemChecks.js +224 -0
  39. package/dist/server/systemSpecimen.d.ts +2 -0
  40. package/dist/server/systemSpecimen.js +49 -0
  41. package/dist/server/themes.d.ts +9 -7
  42. package/dist/server/themes.js +43 -90
  43. package/dist/server/types.d.ts +43 -24
  44. package/dist/server/types.js +3 -1
  45. package/guide/AGENT_SETUP.md +3 -1
  46. package/guide/packs/foundations.md +35 -0
  47. package/guide/topics/asks.md +2 -1
  48. package/guide/topics/feedback.md +5 -3
  49. package/guide/topics/html.md +2 -2
  50. package/guide/topics/http.md +3 -1
  51. package/guide/topics/system.md +157 -0
  52. package/package.json +12 -12
  53. package/skills/mockpit/SKILL.md +7 -4
  54. package/viewer/dist/index.html +9 -8
package/bin/poster.js CHANGED
@@ -8,7 +8,8 @@
8
8
  // capture means the page runs live in the overviews, which is what it did before posters.
9
9
  //
10
10
  // The worker renders the served document and doesn't take a poster's place: one sent by the
11
- // agent wins, and the server refuses a still of a version that has since changed.
11
+ // agent wins, and the server refuses a still of a version that has since changed. A system
12
+ // write retakes the project's stills: one taken under an older revision no longer shows.
12
13
 
13
14
  import { spawn } from "node:child_process";
14
15
  import {
@@ -34,6 +35,8 @@ const CAPTURE_MS = 15_000;
34
35
  const LOAD_MS = 8_000;
35
36
  // A job this old describes a burst long past, and its version has likely moved on.
36
37
  const JOB_MAX_AGE_MS = 2 * 60_000;
38
+ // Stills one system change retakes, latest variants first: the rest run live until then.
39
+ const RETAKE_MAX = 40;
37
40
  // Stays up briefly for the next publish of a burst, which saves starting a browser again.
38
41
  const WORKER_IDLE_MS = 3_000;
39
42
  const WORKER_MAX_MS = 10 * 60_000;
@@ -111,6 +114,12 @@ export function queuePoster(job) {
111
114
  }
112
115
  }
113
116
 
117
+ // { base, token, client, session, project }: after a system write that changed something.
118
+ // The worker reads which variants need a new still, so this never waits either.
119
+ export function queueRetakes(job) {
120
+ queuePoster({ ...job, retake: true });
121
+ }
122
+
114
123
  // --- the browser, over the DevTools protocol on a local port ---
115
124
 
116
125
  async function launchBrowser(path) {
@@ -251,12 +260,48 @@ async function shoot(port, job, url, width) {
251
260
 
252
261
  // --- one job, end to end ---
253
262
 
263
+ const headersOf = (job) => ({
264
+ "content-type": "application/json",
265
+ "x-mockpit-client": job.client,
266
+ ...(job.token ? { authorization: `Bearer ${job.token}` } : {}),
267
+ });
268
+
269
+ // A retake becomes one job per live variant whose page shows no still now: the views
270
+ // already leave out one taken under another revision, and keep the agent's own.
271
+ async function expandRetake(dir, job) {
272
+ const res = await fetch(`${job.base}/api/projects/${encodeURIComponent(job.project)}/tree`, {
273
+ headers: headersOf(job),
274
+ });
275
+ if (!res.ok) return;
276
+ const { tree } = await res.json();
277
+ const wanted = (tree ?? [])
278
+ .filter((d) => !d.archivedAt)
279
+ .flatMap((d) =>
280
+ (d.variants ?? [])
281
+ .filter((v) => v.status !== "archived" && v.surface >= 0 && !v.poster)
282
+ .map((v) => ({ design: d.id, postId: v.postId, version: v.version, at: v.updatedAt })),
283
+ )
284
+ .sort((a, b) => String(b.at).localeCompare(String(a.at)))
285
+ .slice(0, RETAKE_MAX);
286
+ const { retake: _r, project: _p, ...base } = job;
287
+ for (const [i, v] of wanted.entries()) {
288
+ const name = `${Date.now()}-${String(i).padStart(3, "0")}-retake.job`;
289
+ writeFileSync(
290
+ join(dir, name),
291
+ JSON.stringify({
292
+ ...base,
293
+ design: v.design,
294
+ postId: v.postId,
295
+ version: v.version,
296
+ queuedAt: Date.now(),
297
+ }),
298
+ { mode: 0o600 },
299
+ );
300
+ }
301
+ }
302
+
254
303
  async function capture(port, job) {
255
- const headers = {
256
- "content-type": "application/json",
257
- "x-mockpit-client": job.client,
258
- ...(job.token ? { authorization: `Bearer ${job.token}` } : {}),
259
- };
304
+ const headers = headersOf(job);
260
305
  const get = async (path) => {
261
306
  const res = await fetch(`${job.base}${path}`, { headers });
262
307
  if (!res.ok) throw new Error(`${path}: ${res.status}`);
@@ -283,6 +328,7 @@ async function capture(port, job) {
283
328
  ver: String(job.version),
284
329
  ...(mode ? { mode } : {}),
285
330
  ...(rewrites ? { w: String(rewrites) } : {}),
331
+ ...(design.systemRev ? { y: String(design.systemRev) } : {}),
286
332
  });
287
333
  const width = design.widths?.[0] ?? DEFAULT_WIDTH;
288
334
  const data = await shoot(
@@ -299,7 +345,14 @@ async function capture(port, job) {
299
345
  });
300
346
  await post(
301
347
  `/api/designs/${encodeURIComponent(job.design)}/variants/${encodeURIComponent(job.postId)}/poster`,
302
- { version: job.version, rewrites, poster: asset.id, mode, session: job.session },
348
+ {
349
+ version: job.version,
350
+ rewrites,
351
+ poster: asset.id,
352
+ mode,
353
+ system: design.systemRev ?? 0,
354
+ session: job.session,
355
+ },
303
356
  );
304
357
  }
305
358
 
@@ -350,6 +403,10 @@ async function work() {
350
403
  }
351
404
  rmSync(file, { force: true });
352
405
  if (!job || !path || Date.now() - job.queuedAt > JOB_MAX_AGE_MS) continue;
406
+ if (job.retake) {
407
+ await expandRetake(dir, job).catch(() => {});
408
+ continue;
409
+ }
353
410
  try {
354
411
  if (!browser?.alive()) browser = await launchBrowser(path);
355
412
  if (browser) {
@@ -68,6 +68,22 @@ async function api(path, init = {}) {
68
68
  }
69
69
  // Stdio runs on the agent's machine, so a value may name a file: markup and
70
70
  // asset bytes never have to travel through the model's context.
71
+ // A sheet is CSS text when it has a block, else a path: a mistyped path must fail
72
+ // rather than be sent as the project's stylesheet.
73
+ function readSheet(value) {
74
+ if (value === "" || value.includes("{"))
75
+ return value;
76
+ let stat;
77
+ try {
78
+ stat = statSync(value);
79
+ }
80
+ catch {
81
+ throw new Error(`sheet: no file at ${value} (pass CSS text, or a path to a .css file)`);
82
+ }
83
+ if (!stat.isFile())
84
+ throw new Error(`sheet: ${value} is not a file`);
85
+ return readFileSync(value, "utf8");
86
+ }
71
87
  function readMaybeFile(value) {
72
88
  if (value.includes("\n") || value.length > 512)
73
89
  return value;
@@ -284,11 +300,35 @@ const handlers = {
284
300
  }
285
301
  return post(`/api/designs/${enc(args.design)}`, { ...args, project, session }, "PATCH");
286
302
  },
287
- export: (args) => json(`/api/designs/${enc(args.design)}/export${query({
288
- project: resolveProject(args.project),
289
- state: args.state,
290
- variant: args.variant,
291
- })}`),
303
+ async system(args) {
304
+ const session = await ensureSession();
305
+ const { project: named, ...rest } = args;
306
+ const project = resolveProject(named);
307
+ const writes = Object.keys(rest).filter((k) => rest[k] !== undefined);
308
+ if (writes.length === 0)
309
+ return json(`/api/projects/${enc(project)}/system`);
310
+ const body = typeof rest.sheet === "string" ? { ...rest, sheet: readSheet(rest.sheet) } : rest;
311
+ const result = await post(`/api/projects/${enc(project)}/system`, { ...body, session }, "PUT");
312
+ // Every page looks different now: its stills are taken again in the background.
313
+ const taking = result?.changed?.length ? await posters : null;
314
+ if (taking) {
315
+ taking.queueRetakes({
316
+ base: API.replace(/\/$/, ""),
317
+ token: TOKEN,
318
+ client: CLIENT,
319
+ session,
320
+ project,
321
+ });
322
+ }
323
+ return result;
324
+ },
325
+ export: (args) => args.design === undefined
326
+ ? json(`/api/projects/${enc(resolveProject(args.project))}/system/export`)
327
+ : json(`/api/designs/${enc(args.design)}/export${query({
328
+ project: resolveProject(args.project),
329
+ state: args.state,
330
+ variant: args.variant,
331
+ })}`),
292
332
  async upload({ path, data, contentType, filename, kind }) {
293
333
  const session = await ensureSession();
294
334
  // Stdio shares the agent's filesystem, so a path beats base64 in context.
@@ -304,7 +344,9 @@ const handlers = {
304
344
  // agent never restates them; a topic is the same text on every tier.
305
345
  guide: (args) => api(`/agent-howto${query(args.topic === undefined
306
346
  ? { project: resolveProject(args.project) }
307
- : { topic: args.topic })}`),
347
+ : args.topic === "system"
348
+ ? { topic: args.topic, project: resolveProject(args.project) }
349
+ : { topic: args.topic })}`),
308
350
  };
309
351
  // The script travels as a file path when it can, so it is never JSON-escaped
310
352
  // into the model's tool call.