analyzthis_design 2.5.1 → 2.6.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/HOW-TO-USE.md +249 -326
- package/README.md +71 -25
- package/agents/calibration-benchmark.json +187 -0
- package/agents/cards/anuj.md +1 -1
- package/agents/cards/arjun.md +1 -1
- package/agents/cards/kavi.md +2 -1
- package/agents/cards/mood-board.md +7 -7
- package/agents/cards/noor.md +1 -1
- package/agents/cards/priya.md +1 -1
- package/agents/cards/zara.md +3 -3
- package/agents/chain.json +1 -1
- package/agents/design-spec-schema.json +18 -1
- package/agents/manifests/mood-board.json +13 -8
- package/agents/model-policy.json +12 -0
- package/agents/router.json +2 -2
- package/agents/session-schema.json +12 -2
- package/dist/HOW-TO-USE.md +249 -326
- package/dist/README.md +71 -25
- package/dist/agents/calibration-benchmark.json +187 -0
- package/dist/agents/cards/anuj.md +1 -1
- package/dist/agents/cards/arjun.md +1 -1
- package/dist/agents/cards/kavi.md +2 -1
- package/dist/agents/cards/mood-board.md +7 -7
- package/dist/agents/cards/noor.md +1 -1
- package/dist/agents/cards/priya.md +1 -1
- package/dist/agents/cards/zara.md +3 -3
- package/dist/agents/chain.json +1 -1
- package/dist/agents/design-spec-schema.json +18 -1
- package/dist/agents/manifests/mood-board.json +13 -8
- package/dist/agents/model-policy.json +12 -0
- package/dist/agents/router.json +2 -2
- package/dist/agents/session-schema.json +12 -2
- package/dist/bin/cli.js +308 -10
- package/dist/lib/calibrate.js +816 -0
- package/dist/lib/chunk-executor.js +41 -1
- package/dist/lib/chunk-models.js +4 -18
- package/dist/lib/chunk-router.js +22 -6
- package/dist/lib/chunk-run.js +55 -1
- package/dist/lib/chunk-synthesis.js +18 -2
- package/dist/lib/chunk-telemetry.js +77 -17
- package/dist/lib/collect.js +15 -1
- package/dist/lib/cost.js +15 -7
- package/dist/lib/design-spec.js +32 -2
- package/dist/lib/evolution-metrics.js +23 -1
- package/dist/lib/feedback-submit.js +50 -1
- package/dist/lib/feedback.js +15 -0
- package/dist/lib/install.js +1 -0
- package/dist/lib/lessons.js +8 -0
- package/dist/lib/mcp-server.js +189 -7
- package/dist/lib/model-policy.js +174 -0
- package/dist/lib/model-registry.js +71 -0
- package/dist/lib/moodboard-film.js +119 -0
- package/dist/lib/moodboard-view.js +426 -0
- package/dist/lib/moodboard.js +638 -27
- package/dist/lib/orchestrator/run.js +67 -8
- package/dist/lib/outcome.js +7 -2
- package/dist/lib/output-quality.js +95 -0
- package/dist/lib/reference-pack.js +9 -1
- package/dist/lib/session.js +48 -4
- package/dist/lib/system-prompt.js +3 -0
- package/dist/lib/ui-libraries.js +232 -0
- package/dist/skills/anuj/SKILL.md +4 -0
- package/dist/skills/arjun/SKILL.md +2 -0
- package/dist/skills/arjun/references/lens.md +2 -0
- package/dist/skills/design-director/SKILL.md +2 -1
- package/dist/skills/design-reference/SKILL.md +6 -0
- package/dist/skills/design-reference/motion.csv +11 -0
- package/dist/skills/design-reference/schema.json +12 -0
- package/dist/skills/design-reference/stacks/shadcn.csv +6 -0
- package/dist/skills/design-reference/ui-libraries.csv +46 -0
- package/dist/skills/design-spec/SKILL.md +1 -1
- package/dist/skills/getting-started/SKILL.md +2 -2
- package/dist/skills/kavi/SKILL.md +3 -2
- package/dist/skills/meera/SKILL.md +2 -0
- package/dist/skills/mood-board/SKILL.md +94 -87
- package/dist/skills/noor/SKILL.md +4 -0
- package/dist/skills/persona-orchestrator/references/lens.md +4 -4
- package/dist/skills/priya/SKILL.md +5 -0
- package/dist/skills/raj/SKILL.md +2 -0
- package/dist/skills/zara/SKILL.md +49 -4
- package/package.json +13 -2
- package/skills/anuj/SKILL.md +4 -0
- package/skills/arjun/SKILL.md +2 -0
- package/skills/arjun/references/lens.md +2 -0
- package/skills/design-director/SKILL.md +2 -1
- package/skills/design-reference/SKILL.md +6 -0
- package/skills/design-reference/motion.csv +11 -0
- package/skills/design-reference/schema.json +12 -0
- package/skills/design-reference/stacks/shadcn.csv +6 -0
- package/skills/design-reference/ui-libraries.csv +46 -0
- package/skills/design-spec/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +2 -2
- package/skills/kavi/SKILL.md +3 -2
- package/skills/meera/SKILL.md +2 -0
- package/skills/mood-board/SKILL.md +94 -87
- package/skills/noor/SKILL.md +4 -0
- package/skills/persona-orchestrator/references/lens.md +4 -4
- package/skills/priya/SKILL.md +5 -0
- package/skills/raj/SKILL.md +2 -0
- package/skills/zara/SKILL.md +49 -4
|
@@ -109,6 +109,15 @@ function patchIsApplied(patch) {
|
|
|
109
109
|
function collectSignals(projectIds, persona) {
|
|
110
110
|
var outcomes = []; // { value, source }
|
|
111
111
|
var ratings = []; // numbers
|
|
112
|
+
var byModel = {};
|
|
113
|
+
|
|
114
|
+
function modelBucket(attribution) {
|
|
115
|
+
var id = attribution && (attribution.model_id ||
|
|
116
|
+
(attribution.provider && attribution.model && attribution.provider + '/' + attribution.model));
|
|
117
|
+
id = id || 'unknown/unknown';
|
|
118
|
+
if (!byModel[id]) byModel[id] = { outcomes: 0, ratings: 0, rating_total: 0 };
|
|
119
|
+
return byModel[id];
|
|
120
|
+
}
|
|
112
121
|
|
|
113
122
|
for (var i = 0; i < projectIds.length; i++) {
|
|
114
123
|
var state = session.show({ project: projectIds[i] });
|
|
@@ -120,8 +129,10 @@ function collectSignals(projectIds, persona) {
|
|
|
120
129
|
// A confirmed outcome supersedes an inferred one for the same persona.
|
|
121
130
|
if (confirmed[persona] && confirmed[persona].value) {
|
|
122
131
|
outcomes.push({ value: confirmed[persona].value, source: 'confirmed' });
|
|
132
|
+
modelBucket(confirmed[persona].attribution).outcomes++;
|
|
123
133
|
} else if (inferred[persona] && inferred[persona].value) {
|
|
124
134
|
outcomes.push({ value: inferred[persona].value, source: 'inferred' });
|
|
135
|
+
modelBucket(inferred[persona].attribution).outcomes++;
|
|
125
136
|
}
|
|
126
137
|
|
|
127
138
|
var log = state.feedback_log || [];
|
|
@@ -129,10 +140,20 @@ function collectSignals(projectIds, persona) {
|
|
|
129
140
|
if (log[j].persona !== persona) continue;
|
|
130
141
|
if (log[j].rating == null) continue;
|
|
131
142
|
ratings.push(Number(log[j].rating));
|
|
143
|
+
var bucket = modelBucket(log[j].attribution);
|
|
144
|
+
bucket.ratings++;
|
|
145
|
+
bucket.rating_total += Number(log[j].rating);
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
Object.keys(byModel).forEach(function(id) {
|
|
150
|
+
var item = byModel[id];
|
|
151
|
+
item.evidence_count = item.outcomes + item.ratings;
|
|
152
|
+
item.avg_rating = item.ratings ? Math.round((item.rating_total / item.ratings) * 10) / 10 : null;
|
|
153
|
+
item.sufficient_evidence = item.evidence_count >= MIN_EVIDENCE;
|
|
154
|
+
delete item.rating_total;
|
|
155
|
+
});
|
|
156
|
+
return { outcomes: outcomes, ratings: ratings, by_model: byModel };
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
/**
|
|
@@ -243,6 +264,7 @@ function computeEvolutionMetrics(opts) {
|
|
|
243
264
|
outcome_breakdown: outcomeBreakdown,
|
|
244
265
|
ratings: signals.ratings.length,
|
|
245
266
|
avg_rating: avgRating,
|
|
267
|
+
model_breakdown: signals.by_model,
|
|
246
268
|
patches_proposed: personaPatches.length,
|
|
247
269
|
patches_applied: appliedPatches.length,
|
|
248
270
|
avg_signal: Math.round(avgSignal * 10) / 10,
|
|
@@ -240,7 +240,7 @@ function askConsentQuestion() {
|
|
|
240
240
|
rl.question(
|
|
241
241
|
'\nShare anonymized persona feedback with analyzthis_design maintainers?\n'
|
|
242
242
|
+ ' Sends: persona, rating, outcome (shipped/revised/missed), task type,\n'
|
|
243
|
-
+ ' tags,
|
|
243
|
+
+ ' tags, redacted corrections; or text-free fixed-benchmark aggregates\n'
|
|
244
244
|
+ ' Does NOT send: project paths, repo names, emails, or API keys\n'
|
|
245
245
|
+ 'Continue? [y/N] ',
|
|
246
246
|
(answer) => {
|
|
@@ -388,6 +388,53 @@ async function submitScores({ dryRun = false } = {}) {
|
|
|
388
388
|
return { sent, rows };
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Build text-free, fixed-benchmark calibration cells. Random install IDs are
|
|
393
|
+
* deduplication hints only; the server and maintainer must not treat them as identity.
|
|
394
|
+
*/
|
|
395
|
+
function buildCalibrationPayloads(report, cfg) {
|
|
396
|
+
if (!report || report.official_benchmark !== true || report.private_tasks === true) {
|
|
397
|
+
throw new Error('Only the package fixed benchmark can be shared. Private-task results stay local.');
|
|
398
|
+
}
|
|
399
|
+
const cells = Array.isArray(report.community_rows) ? report.community_rows : [];
|
|
400
|
+
return cells.map((cell) => ({
|
|
401
|
+
install_id: cfg.installId,
|
|
402
|
+
package_version: cfg.packageVersion,
|
|
403
|
+
benchmark_version: String(report.benchmark_version || ''),
|
|
404
|
+
validator_version: String(report.validator_version || ''),
|
|
405
|
+
judge_policy_version: String(report.judge_policy_version || ''),
|
|
406
|
+
prompt_hash: String(cell.prompt_hash || ''),
|
|
407
|
+
persona: String(cell.persona || ''),
|
|
408
|
+
effort: String(cell.effort || ''),
|
|
409
|
+
output_format: String(cell.output_format || ''),
|
|
410
|
+
model_id: String(cell.model_id || ''),
|
|
411
|
+
sample_count: Number(cell.sample_count || 0),
|
|
412
|
+
score_mean: Number(cell.score_mean || 0),
|
|
413
|
+
score_variance: Number(cell.score_variance || 0),
|
|
414
|
+
pass_rate: Number(cell.pass_rate || 0),
|
|
415
|
+
latency_ms: Number(cell.latency_ms || 0),
|
|
416
|
+
availability_rate: Number(cell.availability_rate || 0),
|
|
417
|
+
cost_usd: Number(cell.cost_usd || 0),
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function submitCalibration(report, { dryRun = false, yes = false } = {}) {
|
|
422
|
+
const cfg = resolveSubmitConfig();
|
|
423
|
+
if (!cfg.enabled) throw new Error('Feedback submit is disabled.');
|
|
424
|
+
const rows = buildCalibrationPayloads(report, cfg);
|
|
425
|
+
if (!rows.length) return { submitted: 0, skipped: 'no fixed-benchmark cells' };
|
|
426
|
+
if (dryRun) return { submitted: 0, dryRun: true, rows, endpoint: cfg.url };
|
|
427
|
+
const consented = await ensureConsent({ yes });
|
|
428
|
+
if (!consented) return { submitted: 0, cancelled: true };
|
|
429
|
+
let sent = 0;
|
|
430
|
+
for (let i = 0; i < rows.length; i += MAX_ROWS_PER_REQUEST) {
|
|
431
|
+
const batch = rows.slice(i, i + MAX_ROWS_PER_REQUEST);
|
|
432
|
+
await postJson(cfg.url, {}, { kind: 'calibration', rows: batch });
|
|
433
|
+
sent += batch.length;
|
|
434
|
+
}
|
|
435
|
+
return { submitted: sent, endpoint: cfg.url };
|
|
436
|
+
}
|
|
437
|
+
|
|
391
438
|
module.exports = {
|
|
392
439
|
anonymizeText,
|
|
393
440
|
entryToSubmitPayload,
|
|
@@ -395,6 +442,8 @@ module.exports = {
|
|
|
395
442
|
submitFeedback,
|
|
396
443
|
submitScores,
|
|
397
444
|
buildScorePayloads,
|
|
445
|
+
buildCalibrationPayloads,
|
|
446
|
+
submitCalibration,
|
|
398
447
|
submitStatus,
|
|
399
448
|
collectUnsentEntries,
|
|
400
449
|
markEntriesSubmitted,
|
package/dist/lib/feedback.js
CHANGED
|
@@ -75,6 +75,11 @@ function recordFeedback(opts = {}) {
|
|
|
75
75
|
if (!outputEntry) {
|
|
76
76
|
throw new Error(`No output recorded for persona "${persona}" in this session yet.`);
|
|
77
77
|
}
|
|
78
|
+
if (state.active_run && outputEntry.run_id !== state.active_run.id) {
|
|
79
|
+
const stale = new Error(`The "${persona}" output is from an older run. Run it again before recording feedback.`);
|
|
80
|
+
stale.code = 'STALE_OUTPUT';
|
|
81
|
+
throw stale;
|
|
82
|
+
}
|
|
78
83
|
|
|
79
84
|
const entry = {
|
|
80
85
|
id: crypto.randomBytes(8).toString('hex'),
|
|
@@ -86,6 +91,14 @@ function recordFeedback(opts = {}) {
|
|
|
86
91
|
comment: String(comment).trim(),
|
|
87
92
|
correction: String(correction).trim(),
|
|
88
93
|
tags: tags.length ? tags : (satisfied ? ['positive'] : ['other']),
|
|
94
|
+
run_id: outputEntry.run_id || null,
|
|
95
|
+
attribution: outputEntry.attribution || {
|
|
96
|
+
provider: 'host',
|
|
97
|
+
model: 'unknown',
|
|
98
|
+
model_id: 'host/unknown',
|
|
99
|
+
effort: 'unknown',
|
|
100
|
+
},
|
|
101
|
+
quality: outputEntry.quality || null,
|
|
89
102
|
original_output: typeof outputEntry.text === 'string'
|
|
90
103
|
? outputEntry.text.slice(0, 8000)
|
|
91
104
|
: JSON.stringify(outputEntry).slice(0, 8000),
|
|
@@ -155,6 +168,8 @@ function exportCorrections({ persona, project, all = false, output, includePosit
|
|
|
155
168
|
project_id: e.project_id,
|
|
156
169
|
rating: e.rating,
|
|
157
170
|
tags: e.tags,
|
|
171
|
+
run_id: e.run_id || null,
|
|
172
|
+
attribution: e.attribution || null,
|
|
158
173
|
system_card: loadCard(e.persona),
|
|
159
174
|
user: e.context?.task_map_summary || '',
|
|
160
175
|
assistant_rejected: e.original_output,
|
package/dist/lib/install.js
CHANGED
package/dist/lib/lessons.js
CHANGED
|
@@ -82,6 +82,8 @@ function extractLessonsFromSession(state, personaId) {
|
|
|
82
82
|
|
|
83
83
|
var lessons = [];
|
|
84
84
|
var text = entry.text || '';
|
|
85
|
+
var attribution = entry.attribution || null;
|
|
86
|
+
var runId = entry.run_id || null;
|
|
85
87
|
|
|
86
88
|
var fixesMatch = text.match(/Top\s*2\s*fixes:[\s\S]*?(?:\n\n|```|$)/i);
|
|
87
89
|
if (fixesMatch) {
|
|
@@ -103,6 +105,8 @@ function extractLessonsFromSession(state, personaId) {
|
|
|
103
105
|
session_id: state.project_id,
|
|
104
106
|
extracted_at: new Date().toISOString(),
|
|
105
107
|
citation: entry.citations ? entry.citations[0] : ''
|
|
108
|
+
,attribution: attribution
|
|
109
|
+
,run_id: runId
|
|
106
110
|
});
|
|
107
111
|
}
|
|
108
112
|
}
|
|
@@ -127,6 +131,8 @@ function extractLessonsFromSession(state, personaId) {
|
|
|
127
131
|
session_id: state.project_id,
|
|
128
132
|
extracted_at: new Date().toISOString(),
|
|
129
133
|
citation: ''
|
|
134
|
+
,attribution: attribution
|
|
135
|
+
,run_id: runId
|
|
130
136
|
});
|
|
131
137
|
break;
|
|
132
138
|
}
|
|
@@ -147,6 +153,8 @@ function extractLessonsFromSession(state, personaId) {
|
|
|
147
153
|
session_id: state.project_id,
|
|
148
154
|
extracted_at: new Date().toISOString(),
|
|
149
155
|
citation: ''
|
|
156
|
+
,attribution: attribution
|
|
157
|
+
,run_id: runId
|
|
150
158
|
});
|
|
151
159
|
}
|
|
152
160
|
}
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -72,7 +72,7 @@ var TOOLS = [
|
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
name: 'analyzthis_zara',
|
|
75
|
-
description: 'Zara — Delight moments. Identifies exactly ONE structural or surface delight moment
|
|
75
|
+
description: 'Zara — Delight moments, and scout for UI image boards. Identifies exactly ONE structural or surface delight moment. On a UI board she browses real shipped screens, picks 3-5 frames, and re-scouts if the jury skips. Use after the core critique, or when the user wants a UI image board.',
|
|
76
76
|
inputSchema: {
|
|
77
77
|
type: 'object',
|
|
78
78
|
properties: {
|
|
@@ -208,15 +208,56 @@ var TOOLS = [
|
|
|
208
208
|
},
|
|
209
209
|
{
|
|
210
210
|
name: 'analyzthis_mood_board',
|
|
211
|
-
description: '
|
|
211
|
+
description: 'Visual mood board. Zara crops 20-24 real UI elements (cards, tables, type, colour, nav) from 6+ live sites into a sectioned collage with real colours and fonts. Arjun/Noor/Priya/Meera vote as advice. The board opens in the browser and the USER clicks keep / maybe / reject — the user\'s choice overrides every persona, and the locked direction is saved for all personas. kind=mood keeps the older link-and-tag board.',
|
|
212
212
|
inputSchema: {
|
|
213
213
|
type: 'object',
|
|
214
214
|
properties: {
|
|
215
215
|
task: { type: 'string', description: 'Visual direction brief (e.g., "B2B fintech dashboard, trustworthy, high-contrast, data-dense")' },
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
kind: { type: 'string', enum: ['ui', 'mood'], description: 'ui = visual element board (default). mood = older tagged-URL board.' },
|
|
217
|
+
action: { type: 'string', enum: ['create', 'brief', 'capture_script', 'tiles', 'film', 'jury', 'votes', 'show', 'decide', 'rescout', 'lock', 'status', 'frames', 'verdict'], description: 'create (default) → brief → tiles (Zara crops) and film (records urls scrolling as motion tiles; needs playwright-core + Chrome) → jury → votes (advice) → show (user decides in browser) → rescout or lock. decide records a user choice from chat. frames/verdict are older names for tiles/votes.' },
|
|
218
|
+
board: { type: 'string', description: 'Board id' },
|
|
219
|
+
adjectives: { type: 'array', items: { type: 'string' }, description: 'Three feel words for the brief' },
|
|
220
|
+
not_this: { type: 'string', description: 'One thing this board must not look like' },
|
|
221
|
+
sections: { type: 'array', items: { type: 'string', enum: ['first_screen', 'typography', 'color', 'components', 'imagery', 'motion'] }, description: 'Sections this board needs' },
|
|
222
|
+
confirmed_by_user: { type: 'boolean', description: 'User approved the brief (action=brief)' },
|
|
223
|
+
selector: { type: 'string', description: 'CSS selector for action=capture_script' },
|
|
224
|
+
text: { type: 'string', description: 'Visible text that narrows the selector match (action=capture_script)' },
|
|
225
|
+
tiles: {
|
|
226
|
+
type: 'array',
|
|
227
|
+
description: 'Element crops (action=tiles)',
|
|
228
|
+
items: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
page_url: { type: 'string', description: 'Exact page the element came from (required)' },
|
|
232
|
+
image_path: { type: 'string', description: 'Local image file, or the saved Page.captureScreenshot JSON file' },
|
|
233
|
+
image_base64: { type: 'string', description: 'Inline Page.captureScreenshot data when no file was written' },
|
|
234
|
+
image_url: { type: 'string', description: 'Remote image URL if no local crop' },
|
|
235
|
+
video_path: { type: 'string', description: 'Local WebM/MP4 scroll film (motion tiles)' },
|
|
236
|
+
stills: { type: 'array', items: { type: 'string' }, description: 'Up to 4 waypoint still image paths for a film' },
|
|
237
|
+
section: { type: 'string', enum: ['first_screen', 'typography', 'color', 'components', 'imagery', 'motion'] },
|
|
238
|
+
element: { type: 'string', description: 'What the element is, e.g. "pricing card"' },
|
|
239
|
+
tokens: { type: 'object', description: 'From capture_script: colors, fonts, radius, spacing, font_size' },
|
|
240
|
+
steal: { type: 'string' }, refuse: { type: 'string' }, peak: { type: 'string' },
|
|
241
|
+
concept: { type: 'boolean', description: 'True if this is a concept shot, not shipped product' },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
votes: {
|
|
246
|
+
type: 'array',
|
|
247
|
+
description: 'Persona advice (action=votes): [{tile, persona, vote: keep|skip, because}]',
|
|
248
|
+
items: { type: 'object' },
|
|
249
|
+
},
|
|
250
|
+
verdict: { type: 'string', enum: ['SHIP', 'REVISE', 'BLOCK'], description: 'Team advice verdict (action=votes)' },
|
|
251
|
+
summary: { type: 'string', description: 'Team because-lines (action=votes)' },
|
|
252
|
+
tile: { type: 'string', description: 'Tile number or id (action=decide)' },
|
|
253
|
+
decision: { type: 'string', enum: ['keep', 'maybe', 'reject', 'clear'], description: 'User decision (action=decide)' },
|
|
254
|
+
note: { type: 'string', description: 'User note (decide, rescout, lock)' },
|
|
255
|
+
urls: { type: 'array', items: { type: 'string' }, description: 'Reference URLs (kind=mood), or pages to film (action=film)' },
|
|
256
|
+
element: { type: 'string', description: 'Tile name (action=film), default "Scroll film — <site>"' },
|
|
257
|
+
section: { type: 'string', enum: ['first_screen', 'typography', 'color', 'components', 'imagery', 'motion'], description: 'Section for action=film (default motion)' },
|
|
258
|
+
steal: { type: 'string' }, refuse: { type: 'string' }, peak: { type: 'string' },
|
|
259
|
+
auto: { type: 'boolean', description: 'Auto-collect references (kind=mood only)' },
|
|
218
260
|
},
|
|
219
|
-
required: ['task'],
|
|
220
261
|
},
|
|
221
262
|
},
|
|
222
263
|
{
|
|
@@ -466,7 +507,145 @@ function handleCombinationPass(skillId, args) {
|
|
|
466
507
|
return { prompt: prompt, skill: skillId, task: task, persona: skillId, verdicts: 1 };
|
|
467
508
|
}
|
|
468
509
|
|
|
510
|
+
// One live board server per board id, kept for the life of the MCP process.
|
|
511
|
+
var moodBoardServers = {};
|
|
512
|
+
|
|
513
|
+
function tileCountLine(board) {
|
|
514
|
+
var sites = {};
|
|
515
|
+
board.tiles.forEach(function(t) { if (t.site) sites[t.site] = true; });
|
|
516
|
+
return board.tiles.length + ' tile(s) from ' + Object.keys(sites).length + ' site(s)';
|
|
517
|
+
}
|
|
518
|
+
|
|
469
519
|
function handleMoodBoard(args) {
|
|
520
|
+
var task = args.task || '';
|
|
521
|
+
var kind = args.kind === 'mood' ? 'mood' : 'ui';
|
|
522
|
+
var action = args.action || 'create';
|
|
523
|
+
var mb = require('./moodboard');
|
|
524
|
+
var view = require('./moodboard-view');
|
|
525
|
+
var ref = { boardId: args.board };
|
|
526
|
+
var zaraNext = function(board) {
|
|
527
|
+
return 'You are Zara on a Scout Pass. Open real pages in the browser. Do not invent URLs.\n\n' + board.scout.brief +
|
|
528
|
+
'\n\nBoard id: ' + board.board_id +
|
|
529
|
+
'\nToken script for Runtime.evaluate: call analyzthis_mood_board action=capture_script selector=<css>.' +
|
|
530
|
+
'\nSave tiles with action=tiles, board=' + board.board_id + ', tiles=[{page_url,image_path,section,element,tokens,steal,refuse,peak}].' +
|
|
531
|
+
'\nThen action=jury for advice, then action=show and open the URL so the user decides.';
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
if (action === 'capture_script') {
|
|
536
|
+
return { result: mb.captureScript(args.selector || 'body', args.text || '') };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (action === 'brief') {
|
|
540
|
+
var briefed = mb.setBrief({
|
|
541
|
+
boardId: args.board, adjectives: args.adjectives, not_this: args.not_this,
|
|
542
|
+
sections: args.sections, confirmed_by_user: args.confirmed_by_user,
|
|
543
|
+
});
|
|
544
|
+
return { result: 'Brief saved.', board_id: briefed.board.board_id, brief: briefed.board.brief };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (action === 'tiles' || action === 'frames') {
|
|
548
|
+
var tiles = args.tiles || args.frames || [{
|
|
549
|
+
page_url: args.page_url || args.url, element: args.element || args.title, section: args.section,
|
|
550
|
+
image_path: args.image_path, image_url: args.image_url, tokens: args.tokens,
|
|
551
|
+
video_path: args.video_path, stills: args.stills, steal: args.steal, refuse: args.refuse, peak: args.peak,
|
|
552
|
+
}];
|
|
553
|
+
var added = mb.addTiles({ boardId: args.board, tiles: tiles });
|
|
554
|
+
return {
|
|
555
|
+
result: 'Saved ' + added.added.length + ' tile(s). Board now has ' + tileCountLine(added.board) +
|
|
556
|
+
'. Target ' + mb.TILE_TARGET.min + '-' + mb.TILE_TARGET.max + ' from ' + mb.TILE_TARGET.sites + '+ sites.' +
|
|
557
|
+
(added.skipped.length ? ' Skipped: ' + JSON.stringify(added.skipped) : ''),
|
|
558
|
+
board_id: added.board.board_id,
|
|
559
|
+
skipped: added.skipped,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (action === 'jury') {
|
|
564
|
+
var juryBoard = mb.loadUiBoard(ref);
|
|
565
|
+
if (!juryBoard.tiles.length) return { error: 'NEED_SCOUT: Zara has not added tiles yet.' };
|
|
566
|
+
return { prompt: mb.juryPrompt(juryBoard, ''), board_id: juryBoard.board_id };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (action === 'votes' || action === 'verdict') {
|
|
570
|
+
var voted = mb.recordPersonaVotes({
|
|
571
|
+
boardId: args.board, votes: args.votes || [], verdict: args.verdict, summary: args.summary,
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
result: 'Team advice saved' + (voted.ignored.length ? ' (ignored: ' + JSON.stringify(voted.ignored) + ')' : '') +
|
|
575
|
+
'. The user decides next: call action=show and open the URL in the browser.',
|
|
576
|
+
board_id: voted.board.board_id,
|
|
577
|
+
scout: voted.board.scout,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (action === 'film') {
|
|
582
|
+
return require('./moodboard-film').filmTiles({
|
|
583
|
+
boardId: args.board, urls: args.urls || (args.page_url ? [args.page_url] : []),
|
|
584
|
+
section: args.section, element: args.element, steal: args.steal, refuse: args.refuse, peak: args.peak,
|
|
585
|
+
}).then(function(out) {
|
|
586
|
+
var added = out.saved ? out.saved.added : [];
|
|
587
|
+
return {
|
|
588
|
+
result: 'Filmed ' + added.length + ' site(s) as motion tiles' + (out.saved ? '. Board now has ' + tileCountLine(out.saved.board) : '') + '.' +
|
|
589
|
+
(out.failed.length ? ' Failed: ' + JSON.stringify(out.failed) : '') +
|
|
590
|
+
(out.saved && out.saved.skipped.length ? ' Skipped: ' + JSON.stringify(out.saved.skipped) : ''),
|
|
591
|
+
board_id: args.board,
|
|
592
|
+
};
|
|
593
|
+
}).catch(function(e) { return { error: e.message }; });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (action === 'show') {
|
|
597
|
+
var existing = moodBoardServers[args.board];
|
|
598
|
+
if (existing) return { result: 'Board is live at ' + existing.url + ' — open it with browser_navigate.', url: existing.url };
|
|
599
|
+
return view.serveBoard({ boardId: args.board }).then(function(live) {
|
|
600
|
+
moodBoardServers[args.board] = live;
|
|
601
|
+
return {
|
|
602
|
+
result: 'Board is live at ' + live.url + ' — open it with browser_navigate. The user clicks keep / maybe / reject; their choice overrides every persona.',
|
|
603
|
+
url: live.url,
|
|
604
|
+
};
|
|
605
|
+
}).catch(function(e) { return { error: e.message }; });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (action === 'decide') {
|
|
609
|
+
var decided = mb.recordUserDecision({ boardId: args.board, tile: args.tile, decision: args.decision, note: args.note });
|
|
610
|
+
return { result: 'User decision saved on tile ' + args.tile + '. It outranks every persona vote.', board_id: decided.board.board_id };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (action === 'rescout') {
|
|
614
|
+
var again = mb.requestRescout({ boardId: args.board, note: args.note });
|
|
615
|
+
return { prompt: zaraNext(again.board), board_id: again.board.board_id, scout: again.board.scout };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (action === 'lock') {
|
|
619
|
+
var locked = mb.lockDirection({ boardId: args.board, note: args.note });
|
|
620
|
+
return { result: 'Direction locked by the user and saved to the session as design_direction. Every persona must follow it.', direction: locked.board.direction };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (action === 'status') {
|
|
624
|
+
var b = mb.loadUiBoard(ref);
|
|
625
|
+
return { result: JSON.stringify({ board_id: b.board_id, scout: b.scout, brief: b.brief, tiles: tileCountLine(b), direction: mb.computeDirection(b) }, null, 2) };
|
|
626
|
+
}
|
|
627
|
+
} catch (e) {
|
|
628
|
+
return { error: e.message };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (kind === 'ui') {
|
|
632
|
+
if (!task) return { error: 'task is required to open a mood board' };
|
|
633
|
+
return mb.buildMoodBoard({
|
|
634
|
+
task: task, kind: 'ui', discover: false,
|
|
635
|
+
brief: { adjectives: args.adjectives, not_this: args.not_this, sections: args.sections },
|
|
636
|
+
}).then(function(created) {
|
|
637
|
+
return {
|
|
638
|
+
prompt: 'First show the user this brief (job, 3 adjectives, one "not this", sections) and let them correct it with action=brief confirmed_by_user=true.\n\n' + zaraNext(created.board),
|
|
639
|
+
board_id: created.board.board_id,
|
|
640
|
+
brief: created.board.brief,
|
|
641
|
+
};
|
|
642
|
+
}).catch(function(e) { return { error: e.message }; });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return handleLinkMoodBoard(args);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function handleLinkMoodBoard(args) {
|
|
470
649
|
var skill = readSkill('mood-board');
|
|
471
650
|
var task = args.task || '';
|
|
472
651
|
var urls = (args.urls || []).join('\n');
|
|
@@ -857,8 +1036,8 @@ function startStdioServer(catalog) {
|
|
|
857
1036
|
if (method === 'tools/call') {
|
|
858
1037
|
var toolName = params.name;
|
|
859
1038
|
var toolArgs = params.arguments || {};
|
|
860
|
-
|
|
861
|
-
|
|
1039
|
+
// Some tools (mood board create / show) are async.
|
|
1040
|
+
Promise.resolve().then(function() { return handleToolCall(toolName, toolArgs); }).then(function(result) {
|
|
862
1041
|
if (result.error) {
|
|
863
1042
|
send({
|
|
864
1043
|
jsonrpc: '2.0',
|
|
@@ -881,6 +1060,9 @@ function startStdioServer(catalog) {
|
|
|
881
1060
|
},
|
|
882
1061
|
});
|
|
883
1062
|
}
|
|
1063
|
+
}).catch(function(e) {
|
|
1064
|
+
send({ jsonrpc: '2.0', id: id, error: { code: -32603, message: e.message } });
|
|
1065
|
+
});
|
|
884
1066
|
return;
|
|
885
1067
|
}
|
|
886
1068
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared model preference resolver for chunked and unchunked execution.
|
|
5
|
+
* Effort/gate constraints are established by the caller before preferences.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var fs = require('fs');
|
|
9
|
+
var path = require('path');
|
|
10
|
+
var os = require('os');
|
|
11
|
+
var crypto = require('crypto');
|
|
12
|
+
var registry = require('./model-registry');
|
|
13
|
+
var provider = require('./provider');
|
|
14
|
+
var platforms = require('./platforms');
|
|
15
|
+
var outputQuality = require('./output-quality');
|
|
16
|
+
|
|
17
|
+
var PACKAGE_ROOT = platforms.resolvePackageRoot(__dirname);
|
|
18
|
+
var DEFAULT_LOCAL_FILE = path.join(os.homedir(), '.analyzthis_design', 'calibration.json');
|
|
19
|
+
var GLOBAL_FILE = path.join(PACKAGE_ROOT, 'agents', 'model-policy.json');
|
|
20
|
+
|
|
21
|
+
function readJson(file) {
|
|
22
|
+
if (!file || !fs.existsSync(file)) return {};
|
|
23
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
24
|
+
catch (e) { return {}; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function localFile() {
|
|
28
|
+
return process.env.ANALYZTHIS_CALIBRATION_FILE || DEFAULT_LOCAL_FILE;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function promptHash(persona) {
|
|
32
|
+
var manifestPath = path.join(PACKAGE_ROOT, 'agents', 'manifests', persona + '.json');
|
|
33
|
+
var manifest = readJson(manifestPath);
|
|
34
|
+
var parts = [];
|
|
35
|
+
[manifest.system_card, manifest.system_skill].forEach(function(relative) {
|
|
36
|
+
if (!relative) return;
|
|
37
|
+
var file = path.join(PACKAGE_ROOT, relative);
|
|
38
|
+
if (fs.existsSync(file)) parts.push(fs.readFileSync(file));
|
|
39
|
+
});
|
|
40
|
+
return crypto.createHash('sha256').update(Buffer.concat(parts)).digest('hex').slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function calibrationContract() {
|
|
44
|
+
var file = path.join(PACKAGE_ROOT, 'agents', 'calibration-benchmark.json');
|
|
45
|
+
var benchmark = readJson(file);
|
|
46
|
+
return {
|
|
47
|
+
version: benchmark.version,
|
|
48
|
+
judge_policy_version: benchmark.judge_version,
|
|
49
|
+
hash: Object.keys(benchmark).length
|
|
50
|
+
? crypto.createHash('sha256').update(JSON.stringify(benchmark)).digest('hex').slice(0, 16)
|
|
51
|
+
: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function assignmentFrom(policy, persona, effort) {
|
|
56
|
+
var personas = policy.personas || policy.assignments || {};
|
|
57
|
+
return personas[persona] && personas[persona][effort];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function eligible(spec, effort) {
|
|
61
|
+
spec = registry.normalizeSpec(spec);
|
|
62
|
+
if (!spec.provider || !spec.model || registry.isExpired(spec)) return false;
|
|
63
|
+
if (spec.effort && spec.effort.indexOf(effort) === -1) return false;
|
|
64
|
+
if (spec.provider !== 'host' && !provider.hasKey(spec.provider)) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validWinner(winner, persona, effort, requireContract) {
|
|
69
|
+
if (!winner || !eligible(winner, effort)) return false;
|
|
70
|
+
if (requireContract) {
|
|
71
|
+
var hasJudge = winner.judge_policy_version != null || winner.judge_version != null;
|
|
72
|
+
if (!winner.prompt_hash || winner.validator_version == null ||
|
|
73
|
+
winner.benchmark_version == null || !winner.benchmark_hash ||
|
|
74
|
+
!hasJudge || !winner.expires_at) return false;
|
|
75
|
+
}
|
|
76
|
+
if (winner.prompt_hash && winner.prompt_hash !== promptHash(persona)) return false;
|
|
77
|
+
if (winner.validator_version != null && Number(winner.validator_version) !== outputQuality.VERSION) return false;
|
|
78
|
+
var contract = calibrationContract();
|
|
79
|
+
if (winner.benchmark_version != null && winner.benchmark_version !== contract.version) return false;
|
|
80
|
+
if (winner.benchmark_hash && winner.benchmark_hash !== contract.hash) return false;
|
|
81
|
+
var judgeVersion = winner.judge_policy_version != null ? winner.judge_policy_version : winner.judge_version;
|
|
82
|
+
if (judgeVersion != null && judgeVersion !== contract.judge_policy_version) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveAssignment(opts) {
|
|
87
|
+
opts = opts || {};
|
|
88
|
+
var persona = opts.persona || 'general';
|
|
89
|
+
var effort = opts.effort || 'standard';
|
|
90
|
+
var config = opts.config || {};
|
|
91
|
+
var candidates = [];
|
|
92
|
+
|
|
93
|
+
if (opts.explicitProvider || opts.explicitModel) {
|
|
94
|
+
var inferred = opts.explicitProvider;
|
|
95
|
+
if (!inferred && opts.explicitModel) {
|
|
96
|
+
var known = registry.MODELS.find(function(item) { return item.model === opts.explicitModel; });
|
|
97
|
+
inferred = known && known.provider;
|
|
98
|
+
}
|
|
99
|
+
candidates.push({
|
|
100
|
+
source: 'explicit',
|
|
101
|
+
value: { provider: inferred, model: opts.explicitModel },
|
|
102
|
+
strict: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var pinned = assignmentFrom(config.model_policy || {}, persona, effort);
|
|
107
|
+
if (!pinned && config.orchestrator && config.orchestrator.provider &&
|
|
108
|
+
config.orchestrator.provider !== 'auto' && config.orchestrator.model) {
|
|
109
|
+
pinned = { provider: config.orchestrator.provider, model: config.orchestrator.model };
|
|
110
|
+
}
|
|
111
|
+
if (pinned) candidates.push({ source: 'user_config', value: pinned, strict: false });
|
|
112
|
+
candidates.push({ source: 'local_calibration', value: assignmentFrom(readJson(localFile()), persona, effort) });
|
|
113
|
+
candidates.push({ source: 'package_policy', value: assignmentFrom(readJson(GLOBAL_FILE), persona, effort) });
|
|
114
|
+
|
|
115
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
116
|
+
var candidate = candidates[i];
|
|
117
|
+
if (!candidate.value) continue;
|
|
118
|
+
var value = registry.normalizeSpec(candidate.value);
|
|
119
|
+
var requireContract = candidate.source === 'local_calibration' || candidate.source === 'package_policy';
|
|
120
|
+
if (validWinner(value, persona, effort, requireContract)) {
|
|
121
|
+
return { spec: value, source: candidate.source, prompt_hash: promptHash(persona) };
|
|
122
|
+
}
|
|
123
|
+
if (candidate.strict) {
|
|
124
|
+
var error = new Error('Requested model is unavailable or not eligible for effort=' + effort);
|
|
125
|
+
error.code = 'INELIGIBLE_MODEL';
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function prioritizePool(pool, opts) {
|
|
133
|
+
opts = opts || {};
|
|
134
|
+
if (opts.explicitProvider && !opts.explicitModel) {
|
|
135
|
+
var providerOnly = pool.filter(function(item) {
|
|
136
|
+
return item.provider === opts.explicitProvider;
|
|
137
|
+
});
|
|
138
|
+
if (!providerOnly.length) {
|
|
139
|
+
var providerError = new Error('No eligible model for requested provider: ' + opts.explicitProvider);
|
|
140
|
+
providerError.code = 'INELIGIBLE_MODEL';
|
|
141
|
+
throw providerError;
|
|
142
|
+
}
|
|
143
|
+
return { pool: providerOnly, policy: { source: 'explicit_provider' } };
|
|
144
|
+
}
|
|
145
|
+
var resolved = resolveAssignment(opts);
|
|
146
|
+
if (!resolved) return { pool: pool, policy: null };
|
|
147
|
+
var wanted = registry.id(resolved.spec);
|
|
148
|
+
var index = pool.findIndex(function(item) {
|
|
149
|
+
return registry.id(item) === wanted;
|
|
150
|
+
});
|
|
151
|
+
if (index === -1) {
|
|
152
|
+
if (resolved.source === 'explicit' || resolved.source === 'user_config') {
|
|
153
|
+
var error = new Error('Requested model is not in the available model pool: ' + wanted);
|
|
154
|
+
error.code = 'MODEL_NOT_IN_POOL';
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return { pool: pool, policy: null };
|
|
158
|
+
}
|
|
159
|
+
var ordered = pool.slice();
|
|
160
|
+
var selected = ordered.splice(index, 1)[0];
|
|
161
|
+
ordered.unshift(selected);
|
|
162
|
+
return { pool: ordered, policy: resolved };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
VERSION: 1,
|
|
167
|
+
GLOBAL_FILE: GLOBAL_FILE,
|
|
168
|
+
localFile: localFile,
|
|
169
|
+
promptHash: promptHash,
|
|
170
|
+
calibrationContract: calibrationContract,
|
|
171
|
+
eligible: eligible,
|
|
172
|
+
resolveAssignment: resolveAssignment,
|
|
173
|
+
prioritizePool: prioritizePool,
|
|
174
|
+
};
|