mockpit 2.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 (90) hide show
  1. package/README.md +59 -223
  2. package/bin/demoData.js +2 -2
  3. package/bin/initDesign.js +17 -22
  4. package/bin/mockpit.js +626 -216
  5. package/bin/poster.js +434 -0
  6. package/dist/mcp/server.js +127 -29
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +627 -385
  10. package/dist/server/apiViews.js +198 -81
  11. package/dist/server/app.d.ts +7 -2
  12. package/dist/server/app.js +3514 -984
  13. package/dist/server/appIcon.js +15 -5
  14. package/dist/server/appIconPng.js +3 -3
  15. package/dist/server/askNudges.js +1 -1
  16. package/dist/server/cdnInline.d.ts +8 -0
  17. package/dist/server/cdnInline.js +640 -0
  18. package/dist/server/codeWorker.js +2 -2
  19. package/dist/server/contrast.d.ts +4 -0
  20. package/dist/server/contrast.js +157 -0
  21. package/dist/server/decisionList.d.ts +48 -0
  22. package/dist/server/decisionList.js +340 -0
  23. package/dist/server/decisions.d.ts +4 -4
  24. package/dist/server/decisions.js +9 -9
  25. package/dist/server/designGuide.d.ts +14 -7
  26. package/dist/server/designGuide.js +314 -88
  27. package/dist/server/embeds.d.ts +3 -1
  28. package/dist/server/embeds.js +31 -6
  29. package/dist/server/eventLog.d.ts +2 -2
  30. package/dist/server/eventLog.js +11 -11
  31. package/dist/server/events.d.ts +15 -6
  32. package/dist/server/feedbackBatch.d.ts +39 -13
  33. package/dist/server/feedbackBatch.js +129 -47
  34. package/dist/server/fills.d.ts +4 -0
  35. package/dist/server/fills.js +55 -1
  36. package/dist/server/guidance.d.ts +1 -1
  37. package/dist/server/guidance.js +2 -2
  38. package/dist/server/handoff.d.ts +4 -1
  39. package/dist/server/handoff.js +8 -2
  40. package/dist/server/icons.js +1 -1
  41. package/dist/server/index.js +39 -10
  42. package/dist/server/jump.d.ts +21 -0
  43. package/dist/server/jump.js +175 -0
  44. package/dist/server/kits.d.ts +2 -2
  45. package/dist/server/kits.js +54 -1
  46. package/dist/server/knobs.d.ts +3 -3
  47. package/dist/server/knobs.js +6 -6
  48. package/dist/server/mcpHttp.d.ts +7 -1
  49. package/dist/server/mcpHttp.js +29 -12
  50. package/dist/server/mcpSpec.d.ts +27 -27
  51. package/dist/server/mcpSpec.js +196 -56
  52. package/dist/server/parts.js +9 -1
  53. package/dist/server/run.d.ts +1 -1
  54. package/dist/server/run.js +47 -17
  55. package/dist/server/runApi.d.ts +3 -3
  56. package/dist/server/runApi.js +34 -21
  57. package/dist/server/sqlStore.d.ts +148 -103
  58. package/dist/server/sqlStore.js +1052 -181
  59. package/dist/server/standings.d.ts +25 -0
  60. package/dist/server/standings.js +113 -0
  61. package/dist/server/surfacePage.d.ts +8 -9
  62. package/dist/server/surfacePage.js +423 -100
  63. package/dist/server/system.d.ts +234 -0
  64. package/dist/server/system.js +1508 -0
  65. package/dist/server/systemChecks.d.ts +15 -0
  66. package/dist/server/systemChecks.js +224 -0
  67. package/dist/server/systemSpecimen.d.ts +2 -0
  68. package/dist/server/systemSpecimen.js +49 -0
  69. package/dist/server/themes.d.ts +9 -7
  70. package/dist/server/themes.js +43 -90
  71. package/dist/server/types.d.ts +266 -93
  72. package/dist/server/types.js +76 -79
  73. package/extensions/mockpit.js +89 -72
  74. package/guide/AGENT_SETUP.md +15 -1
  75. package/guide/packs/design-craft.md +2 -3
  76. package/guide/packs/foundations.md +35 -0
  77. package/guide/topics/asks.md +108 -63
  78. package/guide/topics/feedback.md +129 -120
  79. package/guide/topics/html.md +18 -25
  80. package/guide/topics/http.md +43 -41
  81. package/guide/topics/knobs.md +7 -10
  82. package/guide/topics/scripts.md +22 -33
  83. package/guide/topics/surfaces.md +39 -48
  84. package/guide/topics/system.md +157 -0
  85. package/opencode/mockpit.js +120 -0
  86. package/opencode/package.json +8 -0
  87. package/package.json +15 -14
  88. package/skills/launch-video/SKILL.md +11 -11
  89. package/skills/mockpit/SKILL.md +33 -26
  90. package/viewer/dist/index.html +10 -9
@@ -1,5 +1,5 @@
1
1
  // Composition: `<mockpit-embed slug="…" variant="…" version="…">` tags in any
2
- // mock's html are expanded server-side by inlining the referenced variant
2
+ // design's html are expanded server-side by inlining the referenced variant
3
3
  // version's first html surface body, recursively, so a component can be built
4
4
  // from components. A tag without `version` follows the version in use when the
5
5
  // variant it resolves to is accepted, else its latest; one without `variant`
@@ -47,7 +47,19 @@ export function parseEmbedTags(html) {
47
47
  return tags;
48
48
  }
49
49
  export const hasEmbedTags = (html) => /<mockpit-embed\b/i.test(html);
50
- // The html with every reference to mock `from` pointed at `to`: the slug of
50
+ // Whether a post's first html surface embeds anything, cached per surfaces array
51
+ // (a stored post never mutates it): a design's view asks on every read.
52
+ const embedsBySurfaces = new WeakMap();
53
+ export function firstHtmlEmbeds(surfaces) {
54
+ const hit = embedsBySurfaces.get(surfaces);
55
+ if (hit !== undefined)
56
+ return hit;
57
+ const html = surfaces.find((x) => x.kind === "html");
58
+ const found = !!html && html.kind === "html" && hasEmbedTags(html.html);
59
+ embedsBySurfaces.set(surfaces, found);
60
+ return found;
61
+ }
62
+ // The html with every reference to design `from` pointed at `to`: the slug of
51
63
  // each embed tag naming it, and each data-goto naming it ("from" or
52
64
  // "from:<state>"). What a slug rename writes into every stored version, so
53
65
  // each version still renders the same component it did.
@@ -64,12 +76,12 @@ export function renameSlugRefs(html, from, to) {
64
76
  })
65
77
  .replace(/(\sdata-goto\s*=\s*)(?:"([^"]*)"|'([^']*)')/gi, (m, key, dq, sq) => {
66
78
  const value = dq ?? sq ?? "";
67
- // "next" and "prev" are directions, never a mock.
79
+ // "next" and "prev" are directions, never a design.
68
80
  if (value === "next" || value === "prev")
69
81
  return m;
70
82
  const colon = value.indexOf(":");
71
- const mock = (colon < 0 ? value : value.slice(0, colon)).trim();
72
- if (mock !== from)
83
+ const design = (colon < 0 ? value : value.slice(0, colon)).trim();
84
+ if (design !== from)
73
85
  return m;
74
86
  const q = dq === undefined ? "'" : '"';
75
87
  return `${key}${q}${to}${colon < 0 ? "" : value.slice(colon)}${q}`;
@@ -77,7 +89,7 @@ export function renameSlugRefs(html, from, to) {
77
89
  }
78
90
  const escapeAttr = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
79
91
  // Replace each tag with its resolved body, expanding that body's own tags in
80
- // turn. `stack` is the chain of slugs being expanded (the root mock's first),
92
+ // turn. `stack` is the chain of slugs being expanded (the root design's first),
81
93
  // so a component that reaches back up renders a marked placeholder instead of
82
94
  // recursing. An unresolvable embed renders as an empty marked-up placeholder
83
95
  // rather than vanishing, so a broken reference is visible in review instead of
@@ -130,6 +142,19 @@ export async function expandHtml(html, resolve, stack) {
130
142
  const out = html.replace(EMBED_TAG, (raw) => bodies[i++] ?? raw);
131
143
  return { html: out, used, stamps, cycle };
132
144
  }
145
+ const STYLE_BLOCK = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
146
+ // Components built on one kit each carry its stylesheet. A repeat would re-declare
147
+ // the kit's rules after what an earlier component set over them and undo it, so
148
+ // only the first copy stays.
149
+ export function dropRepeatedStyles(html) {
150
+ const seen = new Set();
151
+ return html.replace(STYLE_BLOCK, (block) => {
152
+ if (seen.has(block))
153
+ return "";
154
+ seen.add(block);
155
+ return block;
156
+ });
157
+ }
133
158
  // A short, stable key for what a document's embeds resolved to: part of the
134
159
  // render cache key and of the frame URL, so a component's new version reloads
135
160
  // every page that shows it and a stale document is never served as current.
@@ -7,7 +7,7 @@ export interface LogEntry {
7
7
  ms: number;
8
8
  session?: string;
9
9
  project?: string;
10
- mock?: string;
10
+ design?: string;
11
11
  state?: string | null;
12
12
  variant?: string;
13
13
  version?: number;
@@ -21,7 +21,7 @@ export interface LogEntry {
21
21
  }
22
22
  export type LogSink = (entry: LogEntry) => void;
23
23
  export declare function clientOf(header: string | undefined, viewer: boolean, mcp: boolean): string;
24
- export declare function fromRequest(query: URLSearchParams, body: unknown, pathMock: string | undefined): Partial<LogEntry>;
24
+ export declare function fromRequest(query: URLSearchParams, body: unknown, pathDesign: string | undefined): Partial<LogEntry>;
25
25
  export declare function fromResponse(body: unknown): Partial<LogEntry>;
26
26
  export declare function writeLog(sink: LogSink | undefined, entry: LogEntry): void;
27
27
  export declare function logMiddleware(sink: LogSink): MiddlewareHandler;
@@ -18,12 +18,12 @@ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18
18
  const text = (v) => typeof v === "string" && v ? v.slice(0, 200) : undefined;
19
19
  // What the request itself names, for calls whose response doesn't (errors,
20
20
  // feedback reads).
21
- export function fromRequest(query, body, pathMock) {
21
+ export function fromRequest(query, body, pathDesign) {
22
22
  const b = isObj(body) ? body : {};
23
23
  return prune({
24
24
  session: text(query.get("session")) ?? text(b.session),
25
25
  project: text(query.get("project")) ?? text(b.project),
26
- mock: pathMock ?? text(b.mock),
26
+ design: pathDesign ?? text(b.design),
27
27
  variant: text(b.variant),
28
28
  viewing: text(query.get("viewing")),
29
29
  });
@@ -32,7 +32,7 @@ export function fromRequest(query, body, pathMock) {
32
32
  export function fromResponse(body) {
33
33
  if (!isObj(body))
34
34
  return {};
35
- const mock = isObj(body.mock) ? body.mock : undefined;
35
+ const design = isObj(body.design) ? body.design : undefined;
36
36
  const post = isObj(body.post) ? body.post : undefined;
37
37
  // POST /api/sessions answers with the bare session (201 new, 200 reclaimed by key).
38
38
  const session = typeof body.agentSeq === "number" ? body : undefined;
@@ -41,10 +41,10 @@ export function fromResponse(body) {
41
41
  return prune({
42
42
  session: text(body.sessionId) ?? text(post?.sessionId) ?? text(session?.id),
43
43
  key: text(session?.key),
44
- project: text(mock?.project) ?? text(body.project) ?? text(session?.project),
45
- mock: text(mock?.slug) ??
44
+ project: text(design?.project) ?? text(body.project) ?? text(session?.project),
45
+ design: text(design?.slug) ??
46
46
  text(body.slug) ??
47
- (typeof body.mock === "string" ? text(body.mock) : undefined),
47
+ (typeof body.design === "string" ? text(body.design) : undefined),
48
48
  state: post ? (post.state ?? null) : undefined,
49
49
  variant: text(post?.variant),
50
50
  version: typeof post?.version === "number" ? post.version : undefined,
@@ -74,11 +74,11 @@ function deliveredSeqs(feedback) {
74
74
  function pendingLines(pending) {
75
75
  const rows = Array.isArray(pending) ? pending : isObj(pending) ? [pending] : [];
76
76
  return rows.flatMap((p) => {
77
- if (!isObj(p) || typeof p.mock !== "string")
77
+ if (!isObj(p) || typeof p.design !== "string")
78
78
  return [];
79
79
  if (isObj(p.draft))
80
- return [`${p.mock} ${p.draft.answered}/${p.draft.of} answered`];
81
- return p.viewerOpen ? [`${p.mock} open`] : [];
80
+ return [`${p.design} ${p.draft.answered}/${p.draft.of} answered`];
81
+ return p.viewerOpen ? [`${p.design} open`] : [];
82
82
  });
83
83
  }
84
84
  function prune(entry) {
@@ -144,8 +144,8 @@ export function logMiddleware(sink) {
144
144
  Object.assign(entry, fromMcpResult(resBody));
145
145
  }
146
146
  else {
147
- const pathMock = /^\/api\/mocks\/([^/]+)/.exec(c.req.path)?.[1];
148
- Object.assign(entry, fromRequest(url.searchParams, reqBody, pathMock));
147
+ const pathDesign = /^\/api\/designs\/([^/]+)/.exec(c.req.path)?.[1];
148
+ Object.assign(entry, fromRequest(url.searchParams, reqBody, pathDesign));
149
149
  Object.assign(entry, fromResponse(resBody));
150
150
  }
151
151
  writeLog(sink, entry);
@@ -3,33 +3,33 @@ export type FeedEvent = {
3
3
  type: "session-created" | "session-updated" | "session-deleted";
4
4
  id: string;
5
5
  } | {
6
- type: "mock-created" | "mock-updated";
6
+ type: "design-created" | "design-updated";
7
7
  id: string;
8
8
  project: string;
9
9
  } | {
10
- type: "mock-deleted";
10
+ type: "design-deleted";
11
11
  id: string;
12
12
  project: string;
13
13
  } | {
14
14
  type: "draft-updated";
15
- mockId: string;
15
+ designId: string;
16
16
  } | {
17
17
  type: "post-created" | "post-updated";
18
18
  id: string;
19
- mockId: string;
19
+ designId: string;
20
20
  sessionId: string;
21
21
  version: number;
22
22
  by: "agent" | "user";
23
23
  } | {
24
24
  type: "post-deleted";
25
25
  id: string;
26
- mockId: string;
26
+ designId: string;
27
27
  sessionId: string;
28
28
  } | {
29
29
  type: "comment-created";
30
30
  id: string;
31
31
  sessionId: string;
32
- mockId: string | null;
32
+ designId: string | null;
33
33
  postId: string | null;
34
34
  seq: number;
35
35
  } | {
@@ -40,6 +40,15 @@ export type FeedEvent = {
40
40
  type: "comment-delivered";
41
41
  sessionId: string;
42
42
  seq: number;
43
+ } | {
44
+ type: "agent-called";
45
+ sessionId: string;
46
+ } | {
47
+ type: "system-changed";
48
+ project: string;
49
+ rev: number;
50
+ summary: string;
51
+ by: "user" | "agent";
43
52
  } | {
44
53
  type: "theme-changed";
45
54
  mode: Mode;
@@ -1,5 +1,6 @@
1
1
  import { type Decisions } from "./decisions.ts";
2
- import { type AskOption, type Comment, type CommentAnchor, type Mock, type MockEdit, OTHER_ID, type PartComment, type Post, type Reaction, type Reply, type Store } from "./types.ts";
2
+ import { type StandingView } from "./standings.ts";
3
+ import { type AskOption, type Comment, type CommentAnchor, type Design, type DesignEdit, OTHER_ID, type PartComment, type Post, type Reaction, type Reply, type Store } from "./types.ts";
3
4
  export interface VariantRef {
4
5
  state: string | null;
5
6
  variant: string;
@@ -27,14 +28,40 @@ export interface OtherChoice {
27
28
  other: true;
28
29
  images?: ImageRef[];
29
30
  }
30
- export interface AnsweredAsk {
31
+ export interface AnsweredAsk extends StandingView {
31
32
  ask: string;
32
33
  text: string;
33
34
  chosen: (AskOption | OtherChoice)[];
35
+ comment?: string;
36
+ commentImages?: ImageRef[];
37
+ }
38
+ export interface MoreAnswers {
39
+ ask: string;
40
+ text: string;
41
+ existing: string[];
34
42
  note?: string;
35
- noteImages?: ImageRef[];
43
+ like?: string;
44
+ kept?: string[];
45
+ hidden?: {
46
+ label: string;
47
+ why?: string;
48
+ }[];
49
+ answered: {
50
+ ask: string;
51
+ text: string;
52
+ chosen: string[];
53
+ comment?: string;
54
+ }[];
36
55
  }
37
- export interface FeedbackReply extends Omit<Reply, "others" | "notes" | "otherImages" | "noteImages"> {
56
+ export interface UpdateRequest {
57
+ ask: string;
58
+ text: string;
59
+ kept: AskOption;
60
+ inUse: AskOption[];
61
+ }
62
+ export interface FeedbackReply extends Omit<Reply, "others" | "askComments" | "otherImages" | "askCommentImages" | "more" | "standings" | "update"> {
63
+ more?: MoreAnswers;
64
+ update?: UpdateRequest;
38
65
  seq: number;
39
66
  at: string;
40
67
  asks: AnsweredAsk[];
@@ -42,25 +69,24 @@ export interface FeedbackReply extends Omit<Reply, "others" | "notes" | "otherIm
42
69
  decisions?: Decisions;
43
70
  }
44
71
  export interface FeedbackBatch {
45
- mockId: string | null;
72
+ designId: string | null;
46
73
  project: string | null;
47
- mock: string | null;
74
+ design: string | null;
48
75
  title: string | null;
49
76
  reply: FeedbackReply | null;
50
77
  messages: FeedbackMessage[];
51
78
  inUse: InUseRef[];
52
79
  archived: VariantRef[];
53
80
  handoff?: string;
54
- closed?: true;
55
- dismissed?: {
56
- ask: string;
57
- text: string;
58
- }[];
59
- edited?: MockEdit;
81
+ edited?: DesignEdit;
82
+ system?: {
83
+ rev: number;
84
+ changes: string[];
85
+ };
60
86
  }
61
87
  export declare function reactionLine(c: PartComment & {
62
88
  reaction: Reaction;
63
89
  }): string;
64
- export declare function groupFeedback(comments: Comment[], mocks: Map<string, Mock>, posts: Map<string, Post[]>, base?: string): FeedbackBatch[];
90
+ export declare function groupFeedback(comments: Comment[], designs: Map<string, Design>, posts: Map<string, Post[]>, base?: string): FeedbackBatch[];
65
91
  export declare function buildFeedbackBatches(store: Store, comments: Comment[], base: string): Promise<FeedbackBatch[]>;
66
92
  export declare function feedbackImages(batches: FeedbackBatch[]): ImageRef[];
@@ -1,10 +1,60 @@
1
- // The agent-facing shape of feedback: one batch per mock, in delivery order.
1
+ // The agent-facing shape of feedback: one batch per design, in delivery order.
2
2
  // The `feedback` verb, the watch long-poll and the write piggyback all return this,
3
- // so an agent reads one grouped object instead of re-deriving which mock a flat
3
+ // so an agent reads one grouped object instead of re-deriving which design a flat
4
4
  // comment list belongs to. Runtime-agnostic (no node imports).
5
5
  import { decisionsFor } from "./decisions.js";
6
+ import { ROUTED_NOTE } from "./handoff.js";
7
+ import { standingOf, standingView } from "./standings.js";
6
8
  import { builtinAsk, inUseVersion, DEFAULT_VARIANT, OTHER_ID, } from "./types.js";
7
9
  const ref = (p) => ({ state: p.state, variant: p.variant });
10
+ function moreAnswers(design, more, sent) {
11
+ const ask = design.asks.find((a) => a.id === more.ask);
12
+ if (!ask)
13
+ return undefined;
14
+ const st = sent ?? standingOf(design, ask.id);
15
+ const view = standingView(ask, st);
16
+ const like = more.like ? ask.options.find((o) => o.id === more.like)?.label : undefined;
17
+ const labels = (a) => {
18
+ const ids = a.answer === undefined ? [] : Array.isArray(a.answer) ? a.answer : [a.answer];
19
+ return [
20
+ ...ids.map((id) => a.options.find((o) => o.id === id)?.label ?? id),
21
+ ...(a.other === undefined ? [] : [a.other]),
22
+ ];
23
+ };
24
+ return {
25
+ ask: ask.id,
26
+ text: ask.text,
27
+ // Hidden ones are out: asked again, they'd only fold away.
28
+ existing: ask.options.filter((o) => !st.hidden.some((h) => h.id === o.id)).map((o) => o.label),
29
+ ...(more.note ? { note: more.note } : {}),
30
+ ...(like ? { like } : {}),
31
+ ...(view?.kept ? { kept: view.kept.map((k) => k.label) } : {}),
32
+ ...(view?.hidden
33
+ ? { hidden: view.hidden.map((h) => ({ label: h.label, ...(h.why ? { why: h.why } : {}) })) }
34
+ : {}),
35
+ answered: design.asks
36
+ .filter((a) => a.id !== ask.id && labels(a).length > 0)
37
+ .map((a) => ({
38
+ ask: a.id,
39
+ text: a.text,
40
+ chosen: labels(a),
41
+ ...(a.comment ? { comment: a.comment } : {}),
42
+ })),
43
+ };
44
+ }
45
+ function updateRequest(design, wanted) {
46
+ const ask = design.asks.find((a) => a.id === wanted.ask);
47
+ const kept = ask?.options.find((o) => o.id === wanted.option);
48
+ if (!ask || !kept)
49
+ return undefined;
50
+ const ids = ask.answer === undefined ? [] : [ask.answer].flat();
51
+ return {
52
+ ask: ask.id,
53
+ text: ask.text,
54
+ kept,
55
+ inUse: ask.options.filter((o) => ids.includes(o.id)),
56
+ };
57
+ }
8
58
  const REACTION_LEAD = {
9
59
  up: "👍",
10
60
  down: "👎",
@@ -12,7 +62,7 @@ const REACTION_LEAD = {
12
62
  };
13
63
  // `👍 part "hero" (variant B, state Mobile)`, `👎 variant B`, `more like this: the page`.
14
64
  export function reactionLine(c) {
15
- // A mock published without variant names has only "default": naming it is noise.
65
+ // A design published without variant names has only "default": naming it is noise.
16
66
  const named = c.variant && c.variant !== DEFAULT_VARIANT ? c.variant : null;
17
67
  // A version is written as agents write it, dark@2.
18
68
  const at = c.version ? `@${c.version}` : "";
@@ -31,21 +81,32 @@ export function reactionLine(c) {
31
81
  return `${REACTION_LEAD[c.reaction]} ${target}${tail}${note}`;
32
82
  }
33
83
  const isReaction = (c) => !!c.reaction;
34
- function answered(reply, mock, posts, base) {
84
+ function answered(reply, design, posts, base) {
35
85
  const others = reply.others ?? {};
36
- const notes = reply.notes ?? {};
86
+ const comments = reply.askComments ?? {};
37
87
  const refs = (ids) => (ids ?? []).map((id) => ({ id, url: `${base}/a/${encodeURIComponent(id)}` }));
88
+ const standings = reply.standings ?? {};
38
89
  const askIds = new Set([
39
90
  ...Object.keys(reply.answers),
40
91
  ...Object.keys(others),
41
- ...Object.keys(notes),
92
+ ...Object.keys(comments),
93
+ // A question whose answers the user only kept or hid is still news: more answers
94
+ // should follow the kept ones and avoid the hidden.
95
+ ...Object.keys(standings).filter((id) => id !== reply.more?.ask),
42
96
  ]);
43
97
  const out = [];
44
98
  for (const askId of askIds) {
45
- const ask = mock?.asks.find((a) => a.id === askId) ?? (mock && builtinAsk(mock, posts, askId));
99
+ const ask = design?.asks.find((a) => a.id === askId) ?? (design && builtinAsk(design, posts, askId));
46
100
  const answer = reply.answers[askId];
47
101
  const ids = answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
48
- const chosen = ids.map((id) => ask?.options.find((o) => o.id === id) ?? { id, label: id });
102
+ const chosen = ids.map((id) => {
103
+ const option = ask?.options.find((o) => o.id === id);
104
+ if (!option)
105
+ return { id, label: id };
106
+ // What it brought isn't in any version yet: an agent folding it in needs it here.
107
+ const brought = design?.answerAdditions.find((a) => Object.entries(option.set ?? {}).some(([k, v]) => a.knob === k && a.value === v));
108
+ return brought ? { ...option, brought } : option;
109
+ });
49
110
  const otherImages = refs(reply.otherImages?.[askId]);
50
111
  if (others[askId] !== undefined)
51
112
  chosen.push({
@@ -54,38 +115,60 @@ function answered(reply, mock, posts, base) {
54
115
  other: true,
55
116
  ...(otherImages.length ? { images: otherImages } : {}),
56
117
  });
57
- const noteImages = refs(reply.noteImages?.[askId]);
118
+ const commentImages = refs(reply.askCommentImages?.[askId]);
119
+ const st = standings[askId];
58
120
  out.push({
59
121
  ask: askId,
60
122
  text: ask?.text ?? "",
61
123
  chosen,
62
- ...(notes[askId] !== undefined ? { note: notes[askId] } : {}),
63
- ...(noteImages.length ? { noteImages } : {}),
124
+ ...(ask && st ? standingView({ ...ask, answer }, st) : {}),
125
+ ...(comments[askId] !== undefined ? { comment: comments[askId] } : {}),
126
+ ...(commentImages.length ? { commentImages } : {}),
64
127
  });
65
128
  }
66
129
  return out;
67
130
  }
68
131
  // Comments arrive in seq order; keep that order both between batches and inside
69
- // one. A mock gets a fresh batch whenever a second reply arrives for it, so a
132
+ // one. A design gets a fresh batch whenever a second reply arrives for it, so a
70
133
  // batch carries at most one reply and nothing is merged away. A comment whose
71
- // mock is unknown still gets a batch — feedback is never dropped for want of
134
+ // design is unknown still gets a batch — feedback is never dropped for want of
72
135
  // context. `base` (origin + base path) makes image URLs absolute.
73
- export function groupFeedback(comments, mocks, posts, base = "") {
136
+ export function groupFeedback(comments, designs, posts, base = "") {
74
137
  const batches = [];
75
138
  const open = new Map();
76
139
  for (const c of comments) {
77
- const key = c.mockId ?? "";
78
- const mock = c.mockId ? mocks.get(c.mockId) : undefined;
79
- const mockPosts = (c.mockId && posts.get(c.mockId)) || [];
140
+ // Stored from when a close wrote to this stream. Ending something tells the agent
141
+ // nothing, so one still ahead of the cursor stays unsaid.
142
+ if (c.kind === "reply" && c.payload?.archived)
143
+ continue;
144
+ // The user's hand on the system page: one project batch each, never merged.
145
+ const decided = c.kind === "reply" && !c.designId ? c.payload?.system : undefined;
146
+ if (decided) {
147
+ batches.push({
148
+ designId: null,
149
+ project: decided.project,
150
+ design: null,
151
+ title: null,
152
+ reply: null,
153
+ messages: [],
154
+ inUse: [],
155
+ archived: [],
156
+ system: { rev: decided.rev, changes: decided.changes },
157
+ });
158
+ continue;
159
+ }
160
+ const key = c.designId ?? "";
161
+ const design = c.designId ? designs.get(c.designId) : undefined;
162
+ const designPosts = (c.designId && posts.get(c.designId)) || [];
80
163
  let batch = open.get(key);
81
- // An edit says where the mock is now, so it joins whatever batch is open.
164
+ // An edit says where the design is now, so it joins whatever batch is open.
82
165
  const edit = c.kind === "reply" ? c.payload?.edited : undefined;
83
- if (!batch || (c.kind === "reply" && !edit && (batch.reply || batch.closed))) {
166
+ if (!batch || (c.kind === "reply" && !edit && batch.reply)) {
84
167
  batch = {
85
- mockId: c.mockId,
86
- project: mock?.project ?? null,
87
- mock: mock?.slug ?? null,
88
- title: mock?.title ?? null,
168
+ designId: c.designId,
169
+ project: design?.project ?? null,
170
+ design: design?.slug ?? null,
171
+ title: design?.title ?? null,
89
172
  reply: null,
90
173
  messages: [],
91
174
  inUse: [],
@@ -98,36 +181,35 @@ export function groupFeedback(comments, mocks, posts, base = "") {
98
181
  batch.edited = { ...batch.edited, ...edit };
99
182
  continue;
100
183
  }
101
- if (c.kind === "reply" && c.payload?.closed) {
102
- batch.closed = true;
103
- batch.dismissed = c.payload.closed.dismissed.map((id) => ({
104
- ask: id,
105
- text: (mock?.asks.find((a) => a.id === id) ?? (mock && builtinAsk(mock, mockPosts, id)))
106
- ?.text ?? "",
107
- }));
108
- continue;
109
- }
110
184
  if (c.kind === "reply" && c.payload) {
111
- const { others: _others, notes: _notes, otherImages: _otherImages, noteImages: _noteImages, ...payload } = c.payload;
185
+ const { others: _others, askComments: _askComments, otherImages: _otherImages, askCommentImages: _askCommentImages, routed, more: asked, standings: _standings, update: wanted, system: decidedHere, ...payload } = c.payload;
186
+ if (decidedHere)
187
+ batch.system = { rev: decidedHere.rev, changes: decidedHere.changes };
188
+ const more = asked && design ? moreAnswers(design, asked, c.payload.standings?.[asked.ask]) : undefined;
189
+ const update = wanted && design ? updateRequest(design, wanted) : undefined;
190
+ if (routed)
191
+ batch.handoff = ROUTED_NOTE;
112
192
  const reactions = (payload.comments ?? []).filter(isReaction).map(reactionLine);
113
193
  batch.reply = {
114
194
  ...payload,
115
195
  comments: (payload.comments ?? []).filter((x) => !x.reaction),
116
196
  seq: c.seq,
117
197
  at: c.createdAt,
118
- asks: answered(c.payload, mock, mockPosts, base),
198
+ asks: answered(c.payload, design, designPosts, base),
119
199
  ...(reactions.length ? { reactions } : {}),
200
+ ...(more ? { more } : {}),
201
+ ...(update ? { update } : {}),
120
202
  };
121
- const decisions = mock ? decisionsFor(mock, mockPosts, c.payload) : null;
203
+ const decisions = design ? decisionsFor(design, designPosts, c.payload) : null;
122
204
  if (decisions)
123
205
  batch.reply.decisions = decisions;
124
- batch.inUse = mockPosts
206
+ batch.inUse = designPosts
125
207
  .filter((p) => p.status === "accepted")
126
208
  .map((p) => ({ ...ref(p), version: inUseVersion(p) }));
127
- batch.archived = mockPosts.filter((p) => p.status === "archived").map(ref);
209
+ batch.archived = designPosts.filter((p) => p.status === "archived").map(ref);
128
210
  continue;
129
211
  }
130
- const post = c.postId ? mockPosts.find((p) => p.id === c.postId) : undefined;
212
+ const post = c.postId ? designPosts.find((p) => p.id === c.postId) : undefined;
131
213
  batch.messages.push({
132
214
  seq: c.seq,
133
215
  text: c.text,
@@ -142,18 +224,18 @@ export function groupFeedback(comments, mocks, posts, base = "") {
142
224
  return batches;
143
225
  }
144
226
  export async function buildFeedbackBatches(store, comments, base) {
145
- const mocks = new Map();
227
+ const designs = new Map();
146
228
  const posts = new Map();
147
229
  for (const c of comments) {
148
- if (!c.mockId || mocks.has(c.mockId))
230
+ if (!c.designId || designs.has(c.designId))
149
231
  continue;
150
- const mock = await store.getMock(c.mockId);
151
- if (!mock)
232
+ const design = await store.getDesign(c.designId);
233
+ if (!design)
152
234
  continue;
153
- mocks.set(c.mockId, mock);
154
- posts.set(c.mockId, await store.listPosts({ mockId: c.mockId }));
235
+ designs.set(c.designId, design);
236
+ posts.set(c.designId, await store.listPosts({ designId: c.designId }));
155
237
  }
156
- return groupFeedback(comments, mocks, posts, base);
238
+ return groupFeedback(comments, designs, posts, base);
157
239
  }
158
240
  // Every image a list of batches carries, in order — what the MCP tiers attach
159
241
  // as image content so the model sees what the user attached.
@@ -164,8 +246,8 @@ export function feedbackImages(batches) {
164
246
  for (const c of a.chosen)
165
247
  if ("other" in c && c.images)
166
248
  out.push(...c.images);
167
- if (a.noteImages)
168
- out.push(...a.noteImages);
249
+ if (a.commentImages)
250
+ out.push(...a.commentImages);
169
251
  }
170
252
  }
171
253
  return out;
@@ -26,9 +26,13 @@ export interface FillScan {
26
26
  }
27
27
  export declare function scanFills(html: string): FillScan;
28
28
  export declare function slotKnobs(slots: SlotInfo[]): Knobs;
29
+ export declare const cssString: (s: string) => string;
29
30
  export declare function slotCss(slots: SlotInfo[]): string;
30
31
  export declare function composeFills(html: string, chosen: Record<string, string>): string;
31
32
  export declare function slotsInSurfaces(surfaces: Surface[]): SlotInfo[];
33
+ export declare const AS_IS_FILL = "as-is";
34
+ export declare function partOccurrences(html: string, part: string): number;
35
+ export declare function addFill(html: string, part: string, fill: string, markup: string): string;
32
36
  export type FillSplice = {
33
37
  ok: true;
34
38
  surfaces: Surface[];
@@ -165,7 +165,7 @@ export function slotKnobs(slots) {
165
165
  }
166
166
  return out;
167
167
  }
168
- const cssString = (s) => `"${s.replace(/[\\"]/g, (c) => `\\${c}`).replace(/[\n\r<>]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `)}"`;
168
+ export const cssString = (s) => `"${s.replace(/[\\"]/g, (c) => `\\${c}`).replace(/[\n\r<>]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `)}"`;
169
169
  // Shows only the chosen fill of each slot, keyed on the `data-k-<part>`
170
170
  // attribute the knob runtime keeps on <html>. The chosen fill is never styled:
171
171
  // only the others are hidden, so a fill keeps whatever display the agent gave it.
@@ -254,6 +254,60 @@ function markFill(html, part, fill) {
254
254
  const at = m.index + m[0].indexOf("<") + 1 + m[1].length;
255
255
  return html.slice(0, at) + add + html.slice(at);
256
256
  }
257
+ // The name a plain part's own markup goes by once an answer adds a fill to it.
258
+ export const AS_IS_FILL = "as-is";
259
+ // How many elements declare this part, fills counted once each. A fill can only be
260
+ // added to a part that is one thing or already has fills.
261
+ export function partOccurrences(html, part) {
262
+ if (scanAll(html).ranges.some((r) => r.part === part))
263
+ return 1;
264
+ let n = 0;
265
+ walkOpenTags(html, (attrs) => {
266
+ if (attrs["data-part"] === part)
267
+ n++;
268
+ });
269
+ return n;
270
+ }
271
+ function walkOpenTags(html, visit) {
272
+ TOKEN.lastIndex = 0;
273
+ let m;
274
+ while ((m = TOKEN.exec(html))) {
275
+ if (m[0].startsWith("<!--") || m[1])
276
+ continue;
277
+ const tag = m[2].toLowerCase();
278
+ const end = TOKEN.lastIndex;
279
+ visit(attributes(m[3] ?? ""), m.index + 1 + m[2].length);
280
+ TOKEN.lastIndex = end;
281
+ if (RAW.has(tag)) {
282
+ const endTag = new RegExp(`</${tag}\\s*>`, "gi");
283
+ endTag.lastIndex = end;
284
+ const e = endTag.exec(html);
285
+ TOKEN.lastIndex = e ? endTag.lastIndex : html.length;
286
+ }
287
+ }
288
+ }
289
+ // Adds a fill at render time, leaving the html as it was when it can't: a version
290
+ // without this part has nothing to fill, and one with its own fill of this name
291
+ // already says what that value means.
292
+ export function addFill(html, part, fill, markup) {
293
+ const ranges = scanAll(html).ranges.filter((r) => r.part === part);
294
+ if (ranges.some((r) => r.fill === fill))
295
+ return html;
296
+ let base = html;
297
+ if (ranges.length === 0) {
298
+ const at = [];
299
+ walkOpenTags(html, (attrs, tagNameEnd) => {
300
+ if (attrs["data-part"] === part)
301
+ at.push(tagNameEnd);
302
+ });
303
+ if (at.length !== 1)
304
+ return html;
305
+ base = `${html.slice(0, at[0])} data-fill="${AS_IS_FILL}" data-fill-default${html.slice(at[0])}`;
306
+ }
307
+ const spliced = spliceFills([{ kind: "html", html: base }], { [`${part}#${fill}`]: markup });
308
+ const surface = spliced.ok ? spliced.surfaces[0] : undefined;
309
+ return surface?.kind === "html" ? surface.html : html;
310
+ }
257
311
  // Replace one fill of a slot, or add it after the slot's last fill, keyed
258
312
  // "part#fill". Like spliceParts, every target is located in the original
259
313
  // markup and overlapping edits are refused.
@@ -1,4 +1,4 @@
1
- export declare const GUIDE_PACKS: readonly ["design-craft"];
1
+ export declare const GUIDE_PACKS: readonly ["design-craft", "foundations"];
2
2
  export type GuidePack = (typeof GUIDE_PACKS)[number];
3
3
  export declare const RULES_SETTING = "rules";
4
4
  export declare const USER_RULES_SETTING = "userRules";