mockpit 1.3.0 → 3.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 (77) hide show
  1. package/README.md +66 -202
  2. package/bin/demoData.js +5 -5
  3. package/bin/initDesign.js +6 -6
  4. package/bin/mockpit.js +687 -192
  5. package/bin/poster.js +377 -0
  6. package/dist/mcp/server.js +110 -19
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +563 -41
  10. package/dist/server/apiViews.js +223 -74
  11. package/dist/server/app.d.ts +10 -2
  12. package/dist/server/app.js +3284 -782
  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 +638 -0
  18. package/dist/server/codeWorker.js +2 -1
  19. package/dist/server/decisionList.d.ts +45 -0
  20. package/dist/server/decisionList.js +302 -0
  21. package/dist/server/decisions.d.ts +8 -8
  22. package/dist/server/decisions.js +18 -17
  23. package/dist/server/designGuide.d.ts +10 -6
  24. package/dist/server/designGuide.js +173 -78
  25. package/dist/server/embeds.d.ts +4 -1
  26. package/dist/server/embeds.js +57 -6
  27. package/dist/server/eventLog.d.ts +2 -2
  28. package/dist/server/eventLog.js +11 -11
  29. package/dist/server/events.d.ts +9 -6
  30. package/dist/server/feedbackBatch.d.ts +38 -16
  31. package/dist/server/feedbackBatch.js +121 -50
  32. package/dist/server/fills.d.ts +4 -0
  33. package/dist/server/fills.js +55 -1
  34. package/dist/server/guidance.d.ts +23 -0
  35. package/dist/server/guidance.js +71 -0
  36. package/dist/server/handoff.d.ts +4 -1
  37. package/dist/server/handoff.js +8 -2
  38. package/dist/server/icons.js +1 -1
  39. package/dist/server/index.js +49 -11
  40. package/dist/server/jump.d.ts +21 -0
  41. package/dist/server/jump.js +175 -0
  42. package/dist/server/knobs.d.ts +3 -3
  43. package/dist/server/knobs.js +6 -6
  44. package/dist/server/mcpHttp.d.ts +8 -1
  45. package/dist/server/mcpHttp.js +44 -7
  46. package/dist/server/mcpSpec.d.ts +2 -1
  47. package/dist/server/mcpSpec.js +172 -50
  48. package/dist/server/parts.js +9 -1
  49. package/dist/server/postSurfaces.js +3 -0
  50. package/dist/server/run.d.ts +1 -1
  51. package/dist/server/run.js +31 -17
  52. package/dist/server/runApi.d.ts +3 -3
  53. package/dist/server/runApi.js +34 -24
  54. package/dist/server/sqlStore.d.ts +117 -94
  55. package/dist/server/sqlStore.js +1089 -141
  56. package/dist/server/standings.d.ts +25 -0
  57. package/dist/server/standings.js +113 -0
  58. package/dist/server/surfacePage.d.ts +6 -9
  59. package/dist/server/surfacePage.js +361 -86
  60. package/dist/server/types.d.ts +252 -69
  61. package/dist/server/types.js +87 -69
  62. package/extensions/mockpit.js +245 -65
  63. package/guide/AGENT_SETUP.md +13 -1
  64. package/guide/packs/design-craft.md +34 -0
  65. package/guide/topics/asks.md +117 -69
  66. package/guide/topics/feedback.md +156 -119
  67. package/guide/topics/html.md +19 -32
  68. package/guide/topics/http.md +43 -40
  69. package/guide/topics/knobs.md +7 -10
  70. package/guide/topics/scripts.md +24 -35
  71. package/guide/topics/surfaces.md +50 -41
  72. package/opencode/mockpit.js +120 -0
  73. package/opencode/package.json +8 -0
  74. package/package.json +4 -6
  75. package/skills/launch-video/SKILL.md +12 -12
  76. package/skills/mockpit/SKILL.md +35 -23
  77. package/viewer/dist/index.html +9 -8
@@ -0,0 +1,302 @@
1
+ // The project's decision list: what the user has settled, as plain lines any
2
+ // agent can read first, so a new session stops contradicting it. Nothing here
3
+ // summarises and no agent has to keep it: every line is built from what replies
4
+ // already left on the design, the answers on its questions and the variants in
5
+ // use. Those are the current truth, so a later reply on a question replaces its
6
+ // line by construction. Runtime-agnostic (no node imports).
7
+ import { DEFAULT_VARIANT, optionState, } from "./types.js";
8
+ import { standingOf } from "./standings.js";
9
+ export function parseDecisionEdits(raw) {
10
+ if (!raw)
11
+ return {};
12
+ try {
13
+ const value = JSON.parse(raw);
14
+ return value && typeof value === "object" ? value : {};
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ // A write-in or note that hands the question back rather than settling it: the
21
+ // agent still hears it in the reply, but "I don't get this" is no decision. Only
22
+ // plain patterns, so it errs both ways on real replies; the user can remove what
23
+ // slips through. "but" or "i think" rescues an unsure opening, since what follows
24
+ // is usually the real answer ("idk what it means, but it hurt when I stopped").
25
+ const TAKES_A_SIDE = /^((i\s+guess\s+)?(yes|no|yep|nope)\b|(i\s+)?don.?t\s+like\b)/i;
26
+ const UNSURE = /^(idk\s+(what|if|how|why|i\s+already)\b|i\s+don.?t\s+(get|understand|know)\b|i.?m\s+(a\s+bit\s+|even\s+)?(confused|not\s+(even\s+)?sure)\b|not\s+sure\b|this\s+(one\s+)?is\s+(a\s+bit\s+)?confusing\b|so\s+i\s+don.?t\s+get\b)/i;
27
+ const ASKS_BACK = /^(also\s+)?(what|how|why|can|could|is\s+it|do\s+we|does)\b/i;
28
+ function handsItBack(words) {
29
+ const text = words.trim();
30
+ if (TAKES_A_SIDE.test(text))
31
+ return false;
32
+ const sentences = text.match(/[^.!?…]+[.!?…]*/g) ?? [];
33
+ const asking = sentences.filter((s) => s.trim().endsWith("?"));
34
+ if (asking.length > sentences.length / 2)
35
+ return true;
36
+ if (sentences[0]?.trim().endsWith("?") && ASKS_BACK.test(text))
37
+ return true;
38
+ return UNSURE.test(text) && !/\b(but|i\s+think)\b/i.test(text);
39
+ }
40
+ const answerIds = (answer) => answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
41
+ // An older habit of writing the agent's pick into the label; the mark is the
42
+ // option's own now, and in a decision it reads as noise.
43
+ const cleanLabel = (label) => label.replace(/\s*\(recommended\)\s*$/i, "").trim();
44
+ // "Yes. Not: No." says nothing the answer doesn't.
45
+ const YES_NO = /^(yes|no|maybe|sim|não|nao|keep|drop)\b/i;
46
+ const MAX_NOT_CHARS = 90;
47
+ const MAX_NOT = 4;
48
+ // As many of the options left behind as fit, then how many more: enough to stop
49
+ // an agent proposing them again, without the line becoming the option list.
50
+ function notList(labels) {
51
+ const kept = [];
52
+ let used = 0;
53
+ for (const label of labels) {
54
+ if (kept.length === MAX_NOT || (kept.length && used + label.length > MAX_NOT_CHARS))
55
+ break;
56
+ kept.push(label);
57
+ used += label.length;
58
+ }
59
+ if (!kept.length)
60
+ return undefined;
61
+ const rest = labels.length - kept.length;
62
+ return rest ? `${listed(kept)} and ${rest} more` : listed(kept);
63
+ }
64
+ // Labels often hold commas of their own ("Ring, in its own socket").
65
+ function listed(labels) {
66
+ return labels.join(labels.some((l) => l.includes(",")) ? "; " : ", ");
67
+ }
68
+ const stop = (text) => (/[.!?…:]$/.test(text) ? text : `${text}.`);
69
+ function joinParts(p) {
70
+ return [
71
+ p.asked,
72
+ p.chose ? stop(p.chose) : "",
73
+ p.kept ? `Kept for later: ${p.kept}.` : "",
74
+ p.not ? `Not: ${p.not}.` : "",
75
+ p.words ? `“${p.words}”` : "",
76
+ ]
77
+ .filter(Boolean)
78
+ .join(" ");
79
+ }
80
+ function askParts(design, ask) {
81
+ const ids = answerIds(ask.answer);
82
+ const chosen = ask.options.filter((o) => ids.includes(o.id)).map((o) => cleanLabel(o.label));
83
+ const said = [ask.other, ask.comment]
84
+ .map((w) => w?.trim() ?? "")
85
+ .filter((w) => w && !handsItBack(w))
86
+ .join(" ");
87
+ const standing = standingOf(design, ask.id);
88
+ const kept = ask.options
89
+ .filter((o) => standing.kept.includes(o.id) && !ids.includes(o.id))
90
+ .map((o) => cleanLabel(o.label));
91
+ // A reason is taste a new session should know before it offers the same again.
92
+ const why = standing.hidden.filter((h) => h.why);
93
+ if (!chosen.length && !said && !why.length)
94
+ return null;
95
+ const question = ask.text.trim();
96
+ // "Keep this idea?" repeated per state means nothing without the state.
97
+ const state = ask.state && design.states.includes(ask.state) ? ask.state : null;
98
+ const where = state && !question.toLowerCase().includes(state.toLowerCase()) ? `${state}: ` : "";
99
+ const reasoned = why.map((h) => `${cleanLabel(h.label)} (${h.why})`);
100
+ const left = ask.options
101
+ .filter((o) => !ids.includes(o.id) && !standing.kept.includes(o.id))
102
+ .filter((o) => !why.some((h) => h.id === o.id))
103
+ .map((o) => cleanLabel(o.label));
104
+ const not = (chosen.length || reasoned.length) &&
105
+ !ask.other &&
106
+ ![...chosen, ...left].some((l) => YES_NO.test(l))
107
+ ? notList([...reasoned, ...(chosen.length ? left : [])])
108
+ : undefined;
109
+ return {
110
+ asked: `${where}${/[?:.]$/.test(question) ? question : `${question}:`}`,
111
+ ...(chosen.length ? { chose: listed(chosen) } : {}),
112
+ ...(kept.length ? { kept: listed(kept) } : {}),
113
+ ...(not ? { not } : {}),
114
+ ...(said ? { words: said } : {}),
115
+ };
116
+ }
117
+ // The variants a question's answer put in use, by the states it settled.
118
+ function pickedVariants(design, ask) {
119
+ if (ask.scope === "part")
120
+ return [];
121
+ const ids = answerIds(ask.answer);
122
+ return ask.options
123
+ .filter((o) => ids.includes(o.id) && o.variant)
124
+ .flatMap((o) => (o.version !== undefined
125
+ ? [optionState(ask, design)]
126
+ : ask.scope === "state"
127
+ ? [ask.state ?? null]
128
+ : design.states.length
129
+ ? design.states
130
+ : [null]).map((state) => ({ state, variant: o.variant })));
131
+ }
132
+ function useParts(v, siblings) {
133
+ const version = v.version > 1 ? `v${v.acceptedVersion ?? v.version}` : "";
134
+ const named = v.variant === DEFAULT_VARIANT ? "" : v.variant;
135
+ const not = notList(siblings.filter((s) => s.status === "archived").map((s) => s.variant));
136
+ return {
137
+ asked: v.state ? `${v.state}, in use:` : "In use:",
138
+ chose: [named, version].filter(Boolean).join(" ") || "this design",
139
+ ...(not ? { not } : {}),
140
+ };
141
+ }
142
+ const sameQuestion = (ask) => `${ask.state ?? ""}\n${ask.text.trim().toLowerCase()}`;
143
+ // The questions that still stand: an answered one, once per wording (the agent
144
+ // may have re-asked it under a new id, and the later answer is the decision),
145
+ // whose picked variant, if it picked one, was not replaced since. Ask order is
146
+ // the order the agent asked in.
147
+ function standingAsks(design, variants) {
148
+ const seen = new Set();
149
+ const out = [];
150
+ for (const ask of [...design.asks].reverse()) {
151
+ if (ask.dismissed)
152
+ continue;
153
+ const reasons = standingOf(design, ask.id).hidden.some((h) => h.why);
154
+ if (ask.answer === undefined && !ask.other && !ask.comment && !reasons)
155
+ continue;
156
+ const wording = sameQuestion(ask);
157
+ if (seen.has(wording))
158
+ continue;
159
+ seen.add(wording);
160
+ const picks = pickedVariants(design, ask);
161
+ const named = picks.filter((p) => variants.some((v) => v.state === p.state && v.variant === p.variant));
162
+ const replaced = named.length > 0 &&
163
+ !named.some((p) => variants.some((v) => v.state === p.state && v.variant === p.variant && v.status === "accepted"));
164
+ if (!replaced)
165
+ out.unshift(ask);
166
+ }
167
+ return out;
168
+ }
169
+ // Kept and hidden answers change the line, so they're part of what an edit was made on.
170
+ // Absent when there are none, so edits made before standings existed still apply.
171
+ function standingBasis(design, ask) {
172
+ const s = standingOf(design, ask.id);
173
+ if (!s.kept.length && !s.hidden.length)
174
+ return [];
175
+ return [JSON.stringify([s.kept, s.hidden.map((h) => [h.id, h.why ?? ""])])];
176
+ }
177
+ // Every line the project's replies add up to, as derived: newest decided design
178
+ // first, an archived design after every live one, each design's lines as it asked them
179
+ // with what it put in use first.
180
+ export function deriveDecisionLines(designs, sources) {
181
+ const byDesign = new Map();
182
+ for (const v of sources.variants) {
183
+ const list = byDesign.get(v.designId);
184
+ if (list)
185
+ list.push(v);
186
+ else
187
+ byDesign.set(v.designId, [v]);
188
+ }
189
+ const order = [...designs].sort((a, b) => Number(!!a.archivedAt) - Number(!!b.archivedAt) ||
190
+ (sources.decidedAt[b.id] ?? "").localeCompare(sources.decidedAt[a.id] ?? ""));
191
+ const out = [];
192
+ for (const design of order) {
193
+ const variants = byDesign.get(design.id) ?? [];
194
+ const base = {
195
+ design: design.slug,
196
+ title: design.title,
197
+ ...(design.archivedAt ? { closed: true } : {}),
198
+ ...(sources.decidedAt[design.id] ? { at: sources.decidedAt[design.id] } : {}),
199
+ };
200
+ const asks = standingAsks(design, variants);
201
+ const answered = new Set(asks.flatMap((a) => pickedVariants(design, a).map((p) => `${p.state}\n${p.variant}`)));
202
+ for (const v of variants) {
203
+ // The question that put it in use already says so, in better words.
204
+ if (v.status !== "accepted" || answered.has(`${v.state}\n${v.variant}`))
205
+ continue;
206
+ const parts = useParts(v, variants.filter((s) => s.state === v.state && s.variant !== v.variant));
207
+ out.push({
208
+ ...base,
209
+ id: `use:${design.id}:${v.state ?? ""}:${v.variant}`,
210
+ ...(v.state ? { state: v.state } : {}),
211
+ text: joinParts(parts),
212
+ parts,
213
+ basis: `${v.variant}@${v.acceptedVersion ?? v.version}`,
214
+ });
215
+ }
216
+ for (const ask of asks) {
217
+ const parts = askParts(design, ask);
218
+ if (!parts)
219
+ continue;
220
+ out.push({
221
+ ...base,
222
+ id: `ask:${design.id}:${ask.id}`,
223
+ ...(ask.state && design.states.includes(ask.state) ? { state: ask.state } : {}),
224
+ text: joinParts(parts),
225
+ parts,
226
+ basis: JSON.stringify([
227
+ ask.answer ?? null,
228
+ ask.other ?? null,
229
+ ask.comment ?? null,
230
+ ...standingBasis(design, ask),
231
+ ]),
232
+ });
233
+ }
234
+ }
235
+ return out;
236
+ }
237
+ // The hand edits laid over the derived lines. An edit made on an older answer
238
+ // no longer applies: the user has decided again since.
239
+ export function applyDecisionEdits(lines, edits) {
240
+ const out = [];
241
+ for (const { basis, ...line } of lines) {
242
+ const edit = edits[line.id];
243
+ if (!edit || edit.basis !== basis) {
244
+ out.push(line);
245
+ }
246
+ else if (!edit.removed) {
247
+ const { parts: _, ...rest } = line;
248
+ out.push({ ...rest, text: edit.text ?? line.text, edited: true });
249
+ }
250
+ }
251
+ return out;
252
+ }
253
+ // A read must stay cheap to hold in a prompt, and an agent reads often, so it
254
+ // gets the design it asked about and then the newest lines up to a budget, each
255
+ // clipped. A long line is a long write-in, and the rest of it is one call away.
256
+ const AGENT_LINE_CHARS = 280;
257
+ const AGENT_LIST_CHARS = 6000;
258
+ export function agentDecisionList(lines, here) {
259
+ const ordered = here
260
+ ? [...lines.filter((l) => l.design === here), ...lines.filter((l) => l.design !== here)]
261
+ : lines;
262
+ const kept = [];
263
+ let used = 0;
264
+ let clipped = false;
265
+ for (const l of ordered) {
266
+ const long = l.text.length > AGENT_LINE_CHARS;
267
+ const text = long ? `${l.text.slice(0, AGENT_LINE_CHARS - 1)}…` : l.text;
268
+ used += text.length + l.design.length;
269
+ if (used > AGENT_LIST_CHARS && kept.length)
270
+ break;
271
+ clipped ||= long;
272
+ kept.push({
273
+ design: l.design,
274
+ text,
275
+ ...(l.design === here ? { here: true } : {}),
276
+ ...(l.edited ? { edited: true } : {}),
277
+ ...(l.archived ? { archived: true } : {}),
278
+ });
279
+ }
280
+ const left = lines.length - kept.length;
281
+ const cut = [left ? `${left} older lines left out` : "", clipped ? "long lines clipped" : ""]
282
+ .filter(Boolean)
283
+ .join(", ");
284
+ return {
285
+ total: lines.length,
286
+ lines: kept,
287
+ ...(cut ? { more: `${cut}; read with decisions for all of it` } : {}),
288
+ };
289
+ }
290
+ // The list as an agent asked for it whole: nothing cut, without what only the
291
+ // viewer's layout needs.
292
+ export function fullAgentDecisionList(lines) {
293
+ return {
294
+ total: lines.length,
295
+ lines: lines.map((l) => ({
296
+ design: l.design,
297
+ text: l.text,
298
+ ...(l.edited ? { edited: true } : {}),
299
+ ...(l.archived ? { archived: true } : {}),
300
+ })),
301
+ };
302
+ }
@@ -1,23 +1,23 @@
1
1
  import { type SlotInfo } from "./fills.ts";
2
- import { type KnobValue, type Mock, type Post, type Reply } from "./types.ts";
3
- export interface SlotDecision {
2
+ import { type KnobValue, type Design, type Post, type Reply } from "./types.ts";
3
+ export interface FillDecision {
4
4
  fill: string;
5
5
  label?: string;
6
- from?: string;
7
- slots?: Record<string, SlotDecision>;
6
+ borrowedFrom?: string;
7
+ parts?: Record<string, FillDecision>;
8
8
  }
9
9
  export interface StateDecision {
10
10
  variant: string;
11
- slots: Record<string, SlotDecision>;
11
+ parts: Record<string, FillDecision>;
12
12
  }
13
13
  export interface Decisions {
14
14
  states: Record<string, StateDecision>;
15
15
  knobs: Record<string, KnobValue>;
16
16
  }
17
- export declare function replyValues(reply: Pick<Reply, "answers" | "tuned">, mock: Pick<Mock, "asks">): Record<string, KnobValue>;
18
- export declare function settledValues(replies: Pick<Reply, "answers" | "tuned">[], mock: Pick<Mock, "asks" | "knobs">, posts: (Pick<Post, "knobs"> & {
17
+ export declare function replyValues(reply: Pick<Reply, "answers" | "tuned">, design: Pick<Design, "asks">): Record<string, KnobValue>;
18
+ export declare function settledValues(replies: Pick<Reply, "answers" | "tuned">[], design: Pick<Design, "asks" | "knobs">, posts: (Pick<Post, "knobs"> & {
19
19
  surfaces?: Post["surfaces"];
20
20
  })[]): Record<string, KnobValue>;
21
21
  export declare function chosenFills(slots: SlotInfo[], values: Record<string, KnobValue>): Record<string, string>;
22
22
  export declare function postForState(posts: Post[], state: string | null): Post | undefined;
23
- export declare function decisionsFor(mock: Mock, posts: Post[], reply: Pick<Reply, "answers" | "tuned" | "mix">): Decisions | null;
23
+ export declare function decisionsFor(design: Design, posts: Post[], reply: Pick<Reply, "answers" | "tuned" | "borrowed">): Decisions | null;
@@ -1,6 +1,7 @@
1
- // What a reply decided, as the screen is built: per state, the look and each
2
- // slot's chosen fill, with the slots inside a fill listed under it only when
3
- // that fill was chosen; plus every other knob value the reply set. The agent
1
+ // What a reply decided, as the state is built: per state, the variant and the
2
+ // chosen fill of each part with fills, with the parts inside a fill listed
3
+ // under it only when that fill was chosen; plus every other knob value the
4
+ // reply set. The agent
4
5
  // reads "entry → skeleton → rows → shimmer" instead of reassembling it from
5
6
  // option ids, and `export` composes the same choice into one screen.
6
7
  // Runtime-agnostic (no node imports).
@@ -9,9 +10,9 @@ import { checkKnobValue, knobFor } from "./knobs.js";
9
10
  import { inUseVersion, postAtVersion, } from "./types.js";
10
11
  // The knob values a reply carries: each chosen option's `set`, then tuning,
11
12
  // which wins, as in the viewer's resolveKnobs.
12
- export function replyValues(reply, mock) {
13
+ export function replyValues(reply, design) {
13
14
  const out = {};
14
- for (const ask of mock.asks) {
15
+ for (const ask of design.asks) {
15
16
  const answer = reply.answers[ask.id];
16
17
  const ids = answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
17
18
  for (const id of ids)
@@ -22,13 +23,13 @@ export function replyValues(reply, mock) {
22
23
  // Every value the user has sent, oldest reply first so a later answer or tune
23
24
  // replaces an earlier one. Values the knobs declared now can't hold drop out:
24
25
  // the agent may have renamed or narrowed a knob since.
25
- export function settledValues(replies, mock, posts) {
26
+ export function settledValues(replies, design, posts) {
26
27
  const all = {};
27
28
  for (const r of replies)
28
- Object.assign(all, replyValues(r, mock));
29
+ Object.assign(all, replyValues(r, design));
29
30
  const out = {};
30
31
  for (const [path, value] of Object.entries(all)) {
31
- const config = knobFor(path, mock, posts);
32
+ const config = knobFor(path, design, posts);
32
33
  const checked = config === undefined ? null : checkKnobValue(path, config, value);
33
34
  if (checked?.ok)
34
35
  out[path] = checked.value;
@@ -54,7 +55,7 @@ export function postForState(posts, state) {
54
55
  inState[0];
55
56
  return post && (postAtVersion(post, inUseVersion(post)) ?? post);
56
57
  }
57
- function tree(slots, chosen, parent, mix) {
58
+ function tree(slots, chosen, parent, borrowed) {
58
59
  const out = {};
59
60
  for (const s of slots) {
60
61
  const same = parent
@@ -64,23 +65,23 @@ function tree(slots, chosen, parent, mix) {
64
65
  continue;
65
66
  const fill = chosen[s.part];
66
67
  const label = s.fills.find((f) => f.name === fill)?.label;
67
- const inner = tree(slots, chosen, { part: s.part, fill }, mix);
68
+ const inner = tree(slots, chosen, { part: s.part, fill }, borrowed);
68
69
  out[s.part] = {
69
70
  fill,
70
71
  ...(label ? { label } : {}),
71
- ...(mix[s.part] ? { from: mix[s.part] } : {}),
72
- ...(Object.keys(inner).length ? { slots: inner } : {}),
72
+ ...(borrowed[s.part] ? { borrowedFrom: borrowed[s.part] } : {}),
73
+ ...(Object.keys(inner).length ? { parts: inner } : {}),
73
74
  };
74
75
  }
75
76
  return out;
76
77
  }
77
- // Null when the mock has no slots and the reply set no knobs: then the
78
+ // Null when the design has no fills and the reply set no knobs: then the
78
79
  // answered asks already say everything.
79
- export function decisionsFor(mock, posts, reply) {
80
- const values = replyValues(reply, mock);
80
+ export function decisionsFor(design, posts, reply) {
81
+ const values = replyValues(reply, design);
81
82
  const states = {};
82
83
  const slotPaths = new Set();
83
- for (const state of mock.states.length ? mock.states : [null]) {
84
+ for (const state of design.states.length ? design.states : [null]) {
84
85
  const post = postForState(posts, state);
85
86
  if (!post)
86
87
  continue;
@@ -89,7 +90,7 @@ export function decisionsFor(mock, posts, reply) {
89
90
  slotPaths.add(s.part);
90
91
  states[state ?? ""] = {
91
92
  variant: post.variant,
92
- slots: tree(slots, chosenFills(slots, values), null, reply.mix ?? {}),
93
+ parts: tree(slots, chosenFills(slots, values), null, reply.borrowed ?? {}),
93
94
  };
94
95
  }
95
96
  const knobs = Object.fromEntries(Object.entries(values).filter(([p]) => !slotPaths.has(p)));
@@ -1,16 +1,20 @@
1
- import type { DesignSettings } from "./types.ts";
1
+ import type { DesignSystem } from "./types.ts";
2
2
  export declare const GUIDE_TOPICS: readonly ["knobs", "asks", "surfaces", "html", "feedback", "http", "scripts"];
3
3
  export type GuideTopic = (typeof GUIDE_TOPICS)[number];
4
4
  export declare const isGuideTopic: (id: unknown) => id is GuideTopic;
5
5
  export declare const unknownTopicMessage: (topic: string) => string;
6
- export declare function iconsLine(design: DesignSettings | null): string;
6
+ export declare function iconsLine(designSystem: DesignSystem | null): string;
7
7
  /**
8
8
  * What the repo's DESIGN.md, DTCG tokens and shadcn components.json say, as
9
9
  * init stored them. Empty when the repo has none.
10
10
  */
11
- export declare function designFilesSection(design: DesignSettings | null): string;
11
+ export declare function designFilesSection(designSystem: DesignSystem | null): string;
12
12
  /**
13
- * The project-aware brief. `design` is the stored `design:<project>` settings,
14
- * or null for a project that never ran `mockpit init`.
13
+ * The project-aware brief. `designSystem` is the stored `design:<project>` settings,
14
+ * or null for a project that never ran `mockpit init`; `guidance` is the
15
+ * layers above core (server/guidance.ts), absent when none is on.
15
16
  */
16
- export declare function renderBriefGuide(design: DesignSettings | null): string;
17
+ export declare function renderBriefGuide(designSystem: DesignSystem | null, guidance?: {
18
+ line: string;
19
+ body: string;
20
+ }): string;