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.
- package/README.md +66 -202
- package/bin/demoData.js +5 -5
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +687 -192
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +110 -19
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +563 -41
- package/dist/server/apiViews.js +223 -74
- package/dist/server/app.d.ts +10 -2
- package/dist/server/app.js +3284 -782
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +638 -0
- package/dist/server/codeWorker.js +2 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +8 -8
- package/dist/server/decisions.js +18 -17
- package/dist/server/designGuide.d.ts +10 -6
- package/dist/server/designGuide.js +173 -78
- package/dist/server/embeds.d.ts +4 -1
- package/dist/server/embeds.js +57 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +9 -6
- package/dist/server/feedbackBatch.d.ts +38 -16
- package/dist/server/feedbackBatch.js +121 -50
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +49 -11
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +8 -1
- package/dist/server/mcpHttp.js +44 -7
- package/dist/server/mcpSpec.d.ts +2 -1
- package/dist/server/mcpSpec.js +172 -50
- package/dist/server/parts.js +9 -1
- package/dist/server/postSurfaces.js +3 -0
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +31 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -24
- package/dist/server/sqlStore.d.ts +117 -94
- package/dist/server/sqlStore.js +1089 -141
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +6 -9
- package/dist/server/surfacePage.js +361 -86
- package/dist/server/types.d.ts +252 -69
- package/dist/server/types.js +87 -69
- package/extensions/mockpit.js +245 -65
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +34 -0
- package/guide/topics/asks.md +117 -69
- package/guide/topics/feedback.md +156 -119
- package/guide/topics/html.md +19 -32
- package/guide/topics/http.md +43 -40
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +24 -35
- package/guide/topics/surfaces.md +50 -41
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -6
- package/skills/launch-video/SKILL.md +12 -12
- package/skills/mockpit/SKILL.md +35 -23
- 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
|
|
3
|
-
export interface
|
|
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
|
-
|
|
7
|
-
|
|
6
|
+
borrowedFrom?: string;
|
|
7
|
+
parts?: Record<string, FillDecision>;
|
|
8
8
|
}
|
|
9
9
|
export interface StateDecision {
|
|
10
10
|
variant: string;
|
|
11
|
-
|
|
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">,
|
|
18
|
-
export declare function settledValues(replies: Pick<Reply, "answers" | "tuned">[],
|
|
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(
|
|
23
|
+
export declare function decisionsFor(design: Design, posts: Post[], reply: Pick<Reply, "answers" | "tuned" | "borrowed">): Decisions | null;
|
package/dist/server/decisions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// What a reply decided, as the
|
|
2
|
-
//
|
|
3
|
-
// that fill was chosen; plus every other knob value the
|
|
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,
|
|
13
|
+
export function replyValues(reply, design) {
|
|
13
14
|
const out = {};
|
|
14
|
-
for (const ask of
|
|
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,
|
|
26
|
+
export function settledValues(replies, design, posts) {
|
|
26
27
|
const all = {};
|
|
27
28
|
for (const r of replies)
|
|
28
|
-
Object.assign(all, replyValues(r,
|
|
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,
|
|
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,
|
|
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 },
|
|
68
|
+
const inner = tree(slots, chosen, { part: s.part, fill }, borrowed);
|
|
68
69
|
out[s.part] = {
|
|
69
70
|
fill,
|
|
70
71
|
...(label ? { label } : {}),
|
|
71
|
-
...(
|
|
72
|
-
...(Object.keys(inner).length ? {
|
|
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
|
|
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(
|
|
80
|
-
const values = replyValues(reply,
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
11
|
+
export declare function designFilesSection(designSystem: DesignSystem | null): string;
|
|
12
12
|
/**
|
|
13
|
-
* The project-aware brief. `
|
|
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(
|
|
17
|
+
export declare function renderBriefGuide(designSystem: DesignSystem | null, guidance?: {
|
|
18
|
+
line: string;
|
|
19
|
+
body: string;
|
|
20
|
+
}): string;
|