analyzthis_design 2.5.0 → 2.5.2

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 (93) hide show
  1. package/HOW-TO-USE.md +249 -326
  2. package/README.md +58 -13
  3. package/agents/calibration-benchmark.json +187 -0
  4. package/agents/cards/anuj.md +2 -2
  5. package/agents/cards/arjun.md +2 -2
  6. package/agents/cards/kavi.md +2 -1
  7. package/agents/cards/mood-board.md +3 -2
  8. package/agents/cards/noor.md +2 -2
  9. package/agents/cards/priya.md +1 -1
  10. package/agents/cards/zara.md +3 -3
  11. package/agents/chain.json +1 -1
  12. package/agents/design-spec-schema.json +18 -1
  13. package/agents/manifests/mood-board.json +1 -0
  14. package/agents/model-policy.json +12 -0
  15. package/agents/router.json +1 -1
  16. package/agents/session-schema.json +12 -2
  17. package/dist/HOW-TO-USE.md +249 -326
  18. package/dist/README.md +58 -13
  19. package/dist/agents/calibration-benchmark.json +187 -0
  20. package/dist/agents/cards/anuj.md +2 -2
  21. package/dist/agents/cards/arjun.md +2 -2
  22. package/dist/agents/cards/kavi.md +2 -1
  23. package/dist/agents/cards/mood-board.md +3 -2
  24. package/dist/agents/cards/noor.md +2 -2
  25. package/dist/agents/cards/priya.md +1 -1
  26. package/dist/agents/cards/zara.md +3 -3
  27. package/dist/agents/chain.json +1 -1
  28. package/dist/agents/design-spec-schema.json +18 -1
  29. package/dist/agents/manifests/mood-board.json +1 -0
  30. package/dist/agents/model-policy.json +12 -0
  31. package/dist/agents/router.json +1 -1
  32. package/dist/agents/session-schema.json +12 -2
  33. package/dist/bin/cli.js +154 -5
  34. package/dist/lib/calibrate.js +816 -0
  35. package/dist/lib/chunk-executor.js +41 -1
  36. package/dist/lib/chunk-models.js +4 -18
  37. package/dist/lib/chunk-router.js +22 -6
  38. package/dist/lib/chunk-run.js +55 -1
  39. package/dist/lib/chunk-synthesis.js +18 -2
  40. package/dist/lib/chunk-telemetry.js +77 -17
  41. package/dist/lib/collect.js +15 -1
  42. package/dist/lib/cost.js +15 -7
  43. package/dist/lib/design-spec.js +32 -2
  44. package/dist/lib/evolution-metrics.js +23 -1
  45. package/dist/lib/feedback-submit.js +50 -1
  46. package/dist/lib/feedback.js +15 -0
  47. package/dist/lib/lessons.js +8 -0
  48. package/dist/lib/mcp-server.js +119 -5
  49. package/dist/lib/model-policy.js +174 -0
  50. package/dist/lib/model-registry.js +71 -0
  51. package/dist/lib/moodboard.js +162 -28
  52. package/dist/lib/orchestrator/run.js +67 -8
  53. package/dist/lib/outcome.js +7 -2
  54. package/dist/lib/output-quality.js +95 -0
  55. package/dist/lib/reference-pack.js +9 -1
  56. package/dist/lib/session.js +48 -4
  57. package/dist/lib/system-prompt.js +3 -0
  58. package/dist/lib/ui-libraries.js +232 -0
  59. package/dist/skills/anuj/SKILL.md +4 -1
  60. package/dist/skills/arjun/SKILL.md +2 -2
  61. package/dist/skills/arjun/references/lens.md +15 -2
  62. package/dist/skills/design-director/SKILL.md +2 -0
  63. package/dist/skills/design-reference/SKILL.md +6 -0
  64. package/dist/skills/design-reference/motion.csv +11 -0
  65. package/dist/skills/design-reference/schema.json +12 -0
  66. package/dist/skills/design-reference/stacks/shadcn.csv +6 -0
  67. package/dist/skills/design-reference/ui-libraries.csv +46 -0
  68. package/dist/skills/design-reference/ux-guidelines.csv +12 -1
  69. package/dist/skills/design-spec/SKILL.md +1 -1
  70. package/dist/skills/getting-started/SKILL.md +2 -2
  71. package/dist/skills/kavi/SKILL.md +4 -3
  72. package/dist/skills/mood-board/SKILL.md +29 -3
  73. package/dist/skills/noor/SKILL.md +5 -1
  74. package/dist/skills/priya/SKILL.md +3 -0
  75. package/dist/skills/zara/SKILL.md +50 -3
  76. package/package.json +5 -2
  77. package/skills/anuj/SKILL.md +4 -1
  78. package/skills/arjun/SKILL.md +2 -2
  79. package/skills/arjun/references/lens.md +15 -2
  80. package/skills/design-director/SKILL.md +2 -0
  81. package/skills/design-reference/SKILL.md +6 -0
  82. package/skills/design-reference/motion.csv +11 -0
  83. package/skills/design-reference/schema.json +12 -0
  84. package/skills/design-reference/stacks/shadcn.csv +6 -0
  85. package/skills/design-reference/ui-libraries.csv +46 -0
  86. package/skills/design-reference/ux-guidelines.csv +12 -1
  87. package/skills/design-spec/SKILL.md +1 -1
  88. package/skills/getting-started/SKILL.md +2 -2
  89. package/skills/kavi/SKILL.md +4 -3
  90. package/skills/mood-board/SKILL.md +29 -3
  91. package/skills/noor/SKILL.md +5 -1
  92. package/skills/priya/SKILL.md +3 -0
  93. package/skills/zara/SKILL.md +50 -3
@@ -66,11 +66,43 @@ function writeJson(filePath, obj) {
66
66
  fs.writeFileSync(filePath, JSON.stringify(obj, null, 2));
67
67
  }
68
68
 
69
+ function persistBoard(board, workspaceDir) {
70
+ var projectId = board.project_id;
71
+ var bDir = boardDir(projectId, board.board_id);
72
+ board.updated_at = new Date().toISOString();
73
+ writeJson(path.join(bDir, 'board.json'), board);
74
+ var ws = workspaceDir || defaultWorkspaceDir();
75
+ ensureDir(ws);
76
+ var workspacePath = path.join(ws, board.board_id + '.json');
77
+ try { fs.writeFileSync(workspacePath, JSON.stringify(board, null, 2)); }
78
+ catch (e) { /* best effort */ }
79
+ return { board: board, boardDir: bDir, workspacePath: workspacePath };
80
+ }
81
+
82
+ function defaultScoutBrief(task, round, vetoNotes) {
83
+ var lines = [
84
+ 'Zara Scout Pass, round ' + (round || 1) + '.',
85
+ 'Job: ' + String(task).slice(0, 240),
86
+ 'Find 3-5 UI frames. Search Google Images, Dribbble, or Pinterest (or live product pages). Open them. Do not invent URLs. Do not generate AI comps.',
87
+ 'Lens: Peak-End, first 2 seconds. Same job? Peak feeling? One steal, one refuse. Concept shots allowed if you say they are concept. Never sell a Dribbble hero as shipped product.',
88
+ 'Skip generic AI-purple heroes and cinematic GSAP landings unless this job is a marketing page.',
89
+ 'For each frame write: steal (one thing), refuse (one thing), peak (one first impression).',
90
+ 'Then stop. The jury (Arjun, Noor, Priya, Meera) votes. You do not score your own picks.',
91
+ ];
92
+ if (vetoNotes) {
93
+ lines.push('Last jury sent you back. Constraints: ' + String(vetoNotes).slice(0, 500));
94
+ }
95
+ return lines.join('\n');
96
+ }
97
+
69
98
  // ─── Source discovery for mood references ───────────────────────────────────
70
99
 
71
100
  // Known hosts that expose an image or a design article we can fetch as text.
72
101
  var MOOD_REFERENCE_HOSTS = [
73
102
  'dribbble.com',
103
+ 'pinterest.com',
104
+ 'www.pinterest.com',
105
+ 'pin.it',
74
106
  'mobbin.com',
75
107
  'behance.net',
76
108
  'www.behance.net',
@@ -362,7 +394,8 @@ async function buildMoodBoard(opts) {
362
394
 
363
395
  var refs = [];
364
396
  var userRefs = opts.userReferences || [];
365
- var discover = opts.discover !== false;
397
+ var kind = opts.kind === 'ui' ? 'ui' : 'mood';
398
+ var discover = kind === 'ui' ? !!(opts.urls && opts.urls.length) : opts.discover !== false;
366
399
 
367
400
  if (discover) {
368
401
  var plan = discoverReferenceUrls({ task: task, urls: opts.urls || [], config: config });
@@ -397,28 +430,27 @@ async function buildMoodBoard(opts) {
397
430
  var board = {
398
431
  board_id: boardId,
399
432
  project_id: projectId,
433
+ kind: kind,
400
434
  task: task,
401
435
  created_at: new Date().toISOString(),
402
436
  updated_at: new Date().toISOString(),
403
437
  references: refs,
438
+ frames: [],
404
439
  design_system_pack: dsPack,
405
440
  deliberation: null,
406
441
  synthesis: null,
407
442
  user_contributions: userRefs.length,
443
+ scout: kind === 'ui' ? {
444
+ persona: 'zara',
445
+ round: 1,
446
+ max_rounds: 3,
447
+ status: 'awaiting_scout',
448
+ brief: defaultScoutBrief(task, 1, ''),
449
+ last_veto: '',
450
+ } : null,
408
451
  };
409
452
 
410
- writeJson(path.join(bDir, 'board.json'), board);
411
-
412
- // Copy/symlink to workspace so the user can open it directly.
413
- ensureDir(workspaceDir);
414
- var workspacePath = path.join(workspaceDir, boardId + '.json');
415
- try {
416
- fs.writeFileSync(workspacePath, JSON.stringify(board, null, 2));
417
- } catch (e) {
418
- // workspace write is best-effort (e.g. read-only cwd)
419
- }
420
-
421
- return { board: board, boardDir: bDir, workspacePath: workspacePath };
453
+ return persistBoard(board, workspaceDir);
422
454
  }
423
455
 
424
456
  function detectStack(vaultPath) {
@@ -443,17 +475,39 @@ async function critiqueMoodBoard(opts) {
443
475
  var board = loadJson(boardPath);
444
476
  if (!board) throw new Error('Board not found: ' + boardId);
445
477
 
446
- var experts = opts.experts || ['arjun', 'meera', 'priya', 'zara', 'noor'];
478
+ var isUi = board.kind === 'ui';
479
+ var experts = opts.experts || (isUi ? ['arjun', 'meera', 'priya', 'noor'] : ['arjun', 'meera', 'priya', 'zara', 'noor']);
447
480
  var task = opts.task || board.task;
448
481
 
449
- // Build a synthetic critique task that includes the board and DS pack.
450
- var refsSummary = board.references.map(function(r, idx) {
451
- return '[' + (idx + 1) + '] ' + r.title + ' (' + r.type + ') tags=' + r.tags.join(',') + ' — ' + (r.url || r.description).slice(0, 120);
452
- }).join('\n');
482
+ if (isUi && (!board.frames || !board.frames.length)) {
483
+ var need = new Error('Zara has not scouted yet. Add 3-5 frames, then critique.');
484
+ need.code = 'NEED_SCOUT';
485
+ throw need;
486
+ }
487
+
488
+ var refsSummary = isUi
489
+ ? board.frames.map(function(f, idx) {
490
+ return '[' + (idx + 1) + '] ' + f.title + ' round=' + (f.round || 1) +
491
+ ' steal=' + (f.steal || '') + ' refuse=' + (f.refuse || '') +
492
+ ' peak=' + (f.peak || '') + ' — ' + (f.url || '').slice(0, 120);
493
+ }).join('\n')
494
+ : board.references.map(function(r, idx) {
495
+ return '[' + (idx + 1) + '] ' + r.title + ' (' + r.type + ') tags=' + (r.tags || []).join(',') + ' — ' + (r.url || r.description || '').slice(0, 120);
496
+ }).join('\n');
453
497
 
454
498
  var dsText = formatDsPack(board.design_system_pack);
455
499
 
456
- var critiqueTask = [
500
+ var critiqueTask = isUi ? [
501
+ 'UI image-board jury for: ' + task,
502
+ 'Zara scouted these frames. She does not vote. KEEP or SKIP each frame with one because-sentence.',
503
+ 'Do not reward cinematic marketing motion on a daily working surface.',
504
+ '',
505
+ 'Frames:\n' + refsSummary,
506
+ '',
507
+ dsText,
508
+ '',
509
+ 'Use the Honeycomb rigor matrix. Rank the best 2-3 frames. Verdict SHIP only if the set fits the job. REVISE or BLOCK sends Zara to scout again.'
510
+ ].join('\n') : [
457
511
  'Mood-board critique for: ' + task,
458
512
  '',
459
513
  'References:\n' + refsSummary,
@@ -463,7 +517,6 @@ async function critiqueMoodBoard(opts) {
463
517
  'Each persona must use the Honeycomb rigor matrix to evaluate which references are appropriate for the user and why. Output scores, top concerns, and a ranked shortlist of the best 2-3 references. Then participate in adversarial deliberation until consensus.'
464
518
  ].join('\n');
465
519
 
466
- // Use the orchestrator directly, but force the experts to the mood-board set.
467
520
  var result = await orchestratorRun({
468
521
  task: critiqueTask,
469
522
  experts: experts,
@@ -479,17 +532,94 @@ async function critiqueMoodBoard(opts) {
479
532
 
480
533
  board.deliberation = result.deliberation;
481
534
  board.synthesis = result.synthesis;
482
- board.updated_at = new Date().toISOString();
483
535
  board.critique_task = critiqueTask;
536
+ if (isUi) applyJuryOutcome(board, result.synthesis);
484
537
 
485
- writeJson(boardPath, board);
486
- var workspaceDir = opts.workspaceDir || defaultWorkspaceDir();
487
- ensureDir(workspaceDir);
488
- var workspacePath = path.join(workspaceDir, boardId + '.json');
489
- try { fs.writeFileSync(workspacePath, JSON.stringify(board, null, 2)); }
490
- catch (e) { /* best effort */ }
538
+ var saved = persistBoard(board, opts.workspaceDir || defaultWorkspaceDir());
539
+ return { board: saved.board, boardDir: saved.boardDir, workspacePath: saved.workspacePath, orchestratorResult: result };
540
+ }
541
+
542
+ function applyJuryOutcome(board, synthesis) {
543
+ var verdict = String((synthesis && synthesis.verdict) || '').toUpperCase();
544
+ var scout = board.scout || {};
545
+ var maxRounds = scout.max_rounds || 3;
546
+ var notes = '';
547
+ if (synthesis && synthesis.summary) notes = synthesis.summary;
548
+ else if (synthesis && synthesis.top3 && synthesis.top3.join) notes = synthesis.top3.join('; ');
549
+ else notes = verdict;
550
+
551
+ if (verdict === 'SHIP') {
552
+ scout.status = 'done';
553
+ scout.last_veto = '';
554
+ } else if ((scout.round || 1) >= maxRounds) {
555
+ scout.status = 'raj';
556
+ scout.last_veto = String(notes).slice(0, 600);
557
+ scout.brief = 'Max scout rounds reached. Raj or the designer picks from the frames on this board.\n' + scout.last_veto;
558
+ } else {
559
+ scout.round = (scout.round || 1) + 1;
560
+ scout.status = 'awaiting_scout';
561
+ scout.last_veto = String(notes).slice(0, 600);
562
+ scout.brief = defaultScoutBrief(board.task, scout.round, scout.last_veto);
563
+ }
564
+ board.scout = scout;
565
+ }
491
566
 
492
- return { board: board, orchestratorResult: result };
567
+ function addFrames(opts) {
568
+ opts = opts || {};
569
+ var projectId = opts.project || session.getProjectId();
570
+ var boardId = opts.boardId;
571
+ var incoming = opts.frames || (opts.frame ? [opts.frame] : []);
572
+ if (!boardId || !incoming.length) throw new Error('--board and at least one frame are required');
573
+
574
+ var bDir = boardDir(projectId, boardId);
575
+ var board = loadJson(path.join(bDir, 'board.json'));
576
+ if (!board) throw new Error('Board not found: ' + boardId);
577
+ if (board.kind !== 'ui') throw new Error('Frames belong on a --kind ui board.');
578
+
579
+ board.frames = board.frames || [];
580
+ var round = (board.scout && board.scout.round) || 1;
581
+ for (var i = 0; i < incoming.length; i++) {
582
+ var f = incoming[i] || {};
583
+ if (!f.url) continue;
584
+ board.frames.push({
585
+ id: generateBoardId(),
586
+ url: f.url,
587
+ title: f.title || 'Frame',
588
+ steal: f.steal || '',
589
+ refuse: f.refuse || '',
590
+ peak: f.peak || '',
591
+ surface: f.surface || '',
592
+ source: 'zara_scout',
593
+ round: round,
594
+ added_at: new Date().toISOString(),
595
+ });
596
+ }
597
+ if (board.scout) board.scout.status = 'awaiting_jury';
598
+ return persistBoard(board, opts.workspaceDir);
599
+ }
600
+
601
+ function getBoard(opts) {
602
+ opts = opts || {};
603
+ var projectId = opts.project || session.getProjectId();
604
+ var boardId = opts.boardId;
605
+ if (!boardId) throw new Error('board is required');
606
+ var board = loadJson(path.join(boardDir(projectId, boardId), 'board.json'));
607
+ if (!board) throw new Error('Board not found: ' + boardId);
608
+ return board;
609
+ }
610
+
611
+ function recordJuryVerdict(opts) {
612
+ opts = opts || {};
613
+ var board = getBoard(opts);
614
+ if (board.kind !== 'ui') throw new Error('Jury verdict belongs on a ui board.');
615
+ var synthesis = {
616
+ verdict: String(opts.verdict || '').toUpperCase(),
617
+ summary: opts.summary || '',
618
+ top3: opts.top3 || [],
619
+ };
620
+ applyJuryOutcome(board, synthesis);
621
+ board.synthesis = synthesis;
622
+ return persistBoard(board, opts.workspaceDir);
493
623
  }
494
624
 
495
625
  // ─── Add user reference and rerun ─────────────────────────────────────────────
@@ -556,6 +686,10 @@ module.exports = {
556
686
  buildMoodBoard: buildMoodBoard,
557
687
  critiqueMoodBoard: critiqueMoodBoard,
558
688
  addUserReferenceAndRerun: addUserReferenceAndRerun,
689
+ addFrames: addFrames,
690
+ getBoard: getBoard,
691
+ recordJuryVerdict: recordJuryVerdict,
692
+ defaultScoutBrief: defaultScoutBrief,
559
693
  listBoards: listBoards,
560
694
  boardDir: boardDir,
561
695
  formatDsPack: formatDsPack,
@@ -30,6 +30,7 @@ const fs = require('fs');
30
30
  const path = require('path');
31
31
  const os = require('os');
32
32
  const https = require('https');
33
+ const crypto = require('crypto');
33
34
  const session = require('../session');
34
35
  const retrieve = require('../retrieve');
35
36
  const cache = require('../cache');
@@ -40,7 +41,10 @@ const ranker = require('../ranker');
40
41
  const knowledge = require('../knowledge');
41
42
  const chunkRun = require('../chunk-run');
42
43
  const chunkModels = require('../chunk-models');
44
+ const modelPolicy = require('../model-policy');
45
+ const modelRegistry = require('../model-registry');
43
46
  const referencePack = require('../reference-pack');
47
+ const outputQuality = require('../output-quality');
44
48
  const { callHostLlm, createRun, HostLlmPendingError, printDeviInstructions, findLatestRun, loadManifest: loadHostManifest } = require('../host-llm');
45
49
  const { resolveDefaultProvider, resolveTierProvider } = require('../provider');
46
50
  const synthesis = require('../synthesis');
@@ -229,7 +233,7 @@ function classifyEffort({ routing, digest, manifest, scopedMode, fullChain, isDe
229
233
  * 3. chain.tiers[manifest.tier] (legacy fallback, v1.9)
230
234
  * 4. manifest.max_output_tokens (last resort for the cap)
231
235
  */
232
- function resolveModel({ manifest, effort, chain, config, defaultProvider, defaultModel, gateName }) {
236
+ function resolveModel({ manifest, effort, chain, config, defaultProvider, defaultModel, gateName, explicitProvider, explicitModel }) {
233
237
  let effKey = effort;
234
238
  const gateOverride = chain.gate_override;
235
239
  if (gateName && gateOverride && (gateOverride.applies_to || []).includes(gateName)) {
@@ -240,12 +244,35 @@ function resolveModel({ manifest, effort, chain, config, defaultProvider, defaul
240
244
  const matrix = (chain.effort_matrix && chain.effort_matrix[effKey]) || {};
241
245
  const legacy = (config.orchestrator?.tiers && config.orchestrator?.tiers[manifest.tier]) || {};
242
246
 
243
- const provider = override.provider || matrix.provider || legacy.provider || defaultProvider;
244
- const model = override.model || matrix.model || legacy.model || defaultModel;
247
+ let provider = override.provider || matrix.provider || legacy.provider || defaultProvider;
248
+ let model = override.model || matrix.model || legacy.model || defaultModel;
249
+ let requestedModel = explicitModel;
250
+ if (explicitProvider && !requestedModel) {
251
+ if (config.orchestrator?.provider === explicitProvider && config.orchestrator?.model) {
252
+ requestedModel = config.orchestrator.model;
253
+ } else if (explicitProvider === 'ollama') {
254
+ requestedModel = config.ollama?.model || 'qwen2.5';
255
+ } else {
256
+ const known = modelRegistry.MODELS.find((item) =>
257
+ item.provider === explicitProvider && (!item.effort || item.effort.includes(effKey)));
258
+ requestedModel = known?.model;
259
+ }
260
+ }
261
+ const policy = modelPolicy.resolveAssignment({
262
+ persona: manifest.id,
263
+ effort: effKey,
264
+ config,
265
+ explicitProvider,
266
+ explicitModel: requestedModel,
267
+ });
268
+ if (policy) {
269
+ provider = policy.spec.provider;
270
+ model = policy.spec.model;
271
+ }
245
272
  const resolvedProvider = resolveTierProvider(provider, defaultProvider);
246
273
  const maxTokens = override.max_output_tokens || matrix.max_output_tokens
247
274
  || config.orchestrator?.max_tokens?.[manifest.tier] || manifest.max_output_tokens;
248
- return { provider: resolvedProvider, model, maxTokens, effort: effKey };
275
+ return { provider: resolvedProvider, model, maxTokens, effort: effKey, policy: policy && policy.source };
249
276
  }
250
277
 
251
278
  function ollamaJson(baseUrl, model, system, user, maxTokens) {
@@ -500,6 +527,7 @@ async function runUnchunked(opts = {}) {
500
527
  state = session.show({ project: init.projectId });
501
528
  }
502
529
  const projectId = state.project_id;
530
+ const runId = 'run-' + Date.now() + '-' + crypto.randomBytes(4).toString('hex');
503
531
 
504
532
  // Host LLM run directory (Devi bridge — no API keys)
505
533
  let hostContext = null;
@@ -554,6 +582,8 @@ async function runUnchunked(opts = {}) {
554
582
  session.update({
555
583
  project: projectId,
556
584
  patch: {
585
+ active_run: { id: runId, mode: 'unchunked', at: new Date().toISOString() },
586
+ persona_outputs: {},
557
587
  routing_decision: routing,
558
588
  figma_node: figma ? { url: figma, confirmed: false } : state.figma_node,
559
589
  mode: 'assess_only',
@@ -603,8 +633,13 @@ async function runUnchunked(opts = {}) {
603
633
  let cacheHits = 0;
604
634
  let costUsd = 0;
605
635
 
606
- async function loadPersonaContext(personaId, { isProduceRound, isObjectionRound }) {
636
+ async function loadPersonaContext(personaId, { isProduceRound, isObjectionRound, gateName = null }) {
607
637
  const manifest = loadManifest(personaId);
638
+ const effectiveGateName = gateName || (
639
+ personaId === 'arjun' && dsAtRisk.length
640
+ ? 'ds_gate'
641
+ : null
642
+ );
608
643
  const useDeepWireframe = ideation_chain && wireframePersonas.has(personaId);
609
644
  const isReviewRound = isObjectionRound && !isProduceRound;
610
645
 
@@ -645,7 +680,9 @@ async function runUnchunked(opts = {}) {
645
680
  });
646
681
  const resolved = resolveModel({
647
682
  manifest, effort: isObjectionRound ? 'trivial' : effort, chain, config,
648
- defaultProvider: provider, defaultModel: model, gateName: null,
683
+ defaultProvider: provider, defaultModel: model, gateName: effectiveGateName,
684
+ explicitProvider: opts.provider && opts.provider !== 'auto' ? opts.provider : (opts.model ? provider : null),
685
+ explicitModel: opts.model || null,
649
686
  });
650
687
 
651
688
  let finalSystem = system;
@@ -701,7 +738,12 @@ async function runUnchunked(opts = {}) {
701
738
  });
702
739
  const inTok = Math.ceil((system.length + user.length) / 4);
703
740
  const outTok = Math.ceil(text.length / 4);
704
- const price = config.pricing && config.pricing[callOpts.model];
741
+ const actualModel = callOpts.provider === 'host' ? 'unknown' : callOpts.model;
742
+ const quality = outputQuality.validateOutput(text, {
743
+ persona: personaId,
744
+ outputFormat: 'persona_lite',
745
+ });
746
+ const price = callOpts.provider !== 'host' && config.pricing && config.pricing[callOpts.model];
705
747
  if (price) {
706
748
  costUsd += (inTok / 1e6) * (price.input_per_m || 0) + (outTok / 1e6) * (price.output_per_m || 0);
707
749
  }
@@ -712,10 +754,27 @@ async function runUnchunked(opts = {}) {
712
754
  output_tokens_est: outTok,
713
755
  structured_output: parseStructuredOutput(text, personaId),
714
756
  full_prompt: { system, user },
757
+ at: new Date().toISOString(),
758
+ run_id: runId,
759
+ attribution: {
760
+ provider: callOpts.provider,
761
+ model: actualModel,
762
+ model_id: callOpts.provider + '/' + actualModel,
763
+ effort: callOpts.effort || 'standard',
764
+ policy_version: modelPolicy.VERSION,
765
+ prompt_hash: modelPolicy.promptHash(personaId),
766
+ validator_version: outputQuality.VERSION,
767
+ },
768
+ quality: quality,
715
769
  effort_log_entry: {
716
770
  persona: personaId,
717
771
  effort: callOpts.effort || 'standard',
718
- model: callOpts.model,
772
+ provider: callOpts.provider,
773
+ model: actualModel,
774
+ model_id: callOpts.provider + '/' + actualModel,
775
+ input_tokens: inTok,
776
+ output_tokens: outTok,
777
+ run_id: runId,
719
778
  scoped_mode: null,
720
779
  },
721
780
  };
@@ -118,7 +118,9 @@ function inferAllOutcomes(opts) {
118
118
  value: result.inferred,
119
119
  confidence: result.confidence,
120
120
  reason: result.reason,
121
- at: new Date().toISOString()
121
+ at: new Date().toISOString(),
122
+ run_id: entry.run_id || null,
123
+ attribution: entry.attribution || null
122
124
  };
123
125
  session.update({ project: pid, patch: { outcome: outcomePatch } });
124
126
  inferred++;
@@ -143,6 +145,7 @@ function confirmOutcome(opts) {
143
145
 
144
146
  var state = session.show({ project: project });
145
147
  if (!state) throw new Error('No session found');
148
+ var output = state.persona_outputs && state.persona_outputs[persona];
146
149
 
147
150
  var outcomePatch = {
148
151
  inferred: { ...(state.outcome && state.outcome.inferred ? state.outcome.inferred : {}) },
@@ -150,7 +153,9 @@ function confirmOutcome(opts) {
150
153
  };
151
154
  outcomePatch.confirmed[persona] = {
152
155
  value: outcome,
153
- at: new Date().toISOString()
156
+ at: new Date().toISOString(),
157
+ run_id: output && output.run_id || null,
158
+ attribution: output && output.attribution || null
154
159
  };
155
160
  delete outcomePatch.inferred[persona];
156
161
 
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Format-aware output validation shared by normal runs and calibration.
5
+ * Hard failures may trigger a bounded retry; warnings only affect quality score.
6
+ */
7
+
8
+ var VERSION = 1;
9
+
10
+ var PERSONA_FORBIDDEN = {
11
+ arjun: [/delight pass/i, /ARR impact/i],
12
+ meera: [/color contrast/i, /typography scale/i, /WCAG/i],
13
+ priya: [/delight moment/i, /north-star metric/i],
14
+ zara: [/--color-primary/i, /contrast ratio/i, /fix the token/i],
15
+ };
16
+
17
+ function issue(code, message, hard) {
18
+ return { code: code, message: message, hard: !!hard };
19
+ }
20
+
21
+ function validateOutput(text, opts) {
22
+ opts = opts || {};
23
+ text = typeof text === 'string' ? text.trim() : '';
24
+ var format = String(opts.outputFormat || opts.format || 'generic').toLowerCase();
25
+ var persona = String(opts.persona || '').toLowerCase();
26
+ var issues = [];
27
+
28
+ if (!text) issues.push(issue('empty', 'The model returned no text.', true));
29
+ if (text && text.length < 40) issues.push(issue('too_short', 'The answer is too short to evaluate.', true));
30
+
31
+ if (format === 'persona_lite' || format === 'lite') {
32
+ if (!/##\s*[A-Za-z]+(?:\s*[—-]\s*Lite)?/i.test(text)) {
33
+ issues.push(issue('missing_header', 'Missing persona output header.', false));
34
+ }
35
+ if (!/Top\s*2|fix|verdict|no delight needed/i.test(text)) {
36
+ issues.push(issue('missing_action', 'Missing an actionable finding or verdict.', true));
37
+ }
38
+ } else if (format === 'ux_audit') {
39
+ if (!/usab|access|hierarch|visual|UX/i.test(text)) {
40
+ issues.push(issue('missing_audit', 'UX audit has no UX or visual assessment.', true));
41
+ }
42
+ if (!/fix|recommend|change|keep|revise|ship|block/i.test(text)) {
43
+ issues.push(issue('missing_action', 'UX audit has no actionable conclusion.', true));
44
+ }
45
+ } else if (format === 'premise_assessment' || format === 'premise_check') {
46
+ if (!/premise|problem|assumption|framing|right problem/i.test(text)) {
47
+ issues.push(issue('missing_premise', 'Premise assessment does not examine the framing.', true));
48
+ }
49
+ if (!/proceed|change|reject|accept|revise|evidence/i.test(text)) {
50
+ issues.push(issue('missing_decision', 'Premise assessment has no decision or evidence request.', true));
51
+ }
52
+ } else if (format === 'synthesis') {
53
+ if (!/\b(SHIP|REVISE|BLOCK)\b/i.test(text)) {
54
+ issues.push(issue('missing_verdict', 'Synthesis has no SHIP, REVISE, or BLOCK verdict.', true));
55
+ }
56
+ if (!/top\s*3|action|next step|fix/i.test(text)) {
57
+ issues.push(issue('missing_actions', 'Synthesis has no next actions.', false));
58
+ }
59
+ }
60
+
61
+ var forbidden = PERSONA_FORBIDDEN[persona] || [];
62
+ for (var i = 0; i < forbidden.length; i++) {
63
+ if (forbidden[i].test(text)) {
64
+ issues.push(issue('forbidden_scope', 'The persona crossed into a forbidden scope.', true));
65
+ }
66
+ }
67
+
68
+ var hardFailures = issues.filter(function(item) { return item.hard; });
69
+ var warnings = issues.filter(function(item) { return !item.hard; });
70
+ var score = Math.max(0, 1 - hardFailures.length * 0.35 - warnings.length * 0.1);
71
+ return {
72
+ version: VERSION,
73
+ format: format,
74
+ pass: hardFailures.length === 0,
75
+ score: Number(score.toFixed(2)),
76
+ issues: issues,
77
+ hard_failures: hardFailures.length,
78
+ warnings: warnings.length,
79
+ };
80
+ }
81
+
82
+ function classifyError(error) {
83
+ var message = String(error && error.message ? error.message : error || '');
84
+ if (/429|rate.?limit|too many requests|quota/i.test(message)) return 'rate_limit';
85
+ if (/timeout|timed out|ECONNRESET|ENOTFOUND|network|socket/i.test(message)) return 'transport';
86
+ if (/401|403|unauthori|forbidden|api.?key/i.test(message)) return 'auth';
87
+ return 'provider';
88
+ }
89
+
90
+ module.exports = {
91
+ VERSION: VERSION,
92
+ PERSONA_FORBIDDEN: PERSONA_FORBIDDEN,
93
+ validateOutput: validateOutput,
94
+ classifyError: classifyError,
95
+ };
@@ -37,6 +37,7 @@ var REFERENCE_MAP = {
37
37
  { file: 'ux-guidelines.csv', column: 'Category', limit: 3 },
38
38
  { file: 'ui-reasoning.csv', column: 'UI Category', limit: 3 },
39
39
  { file: 'charts.csv', column: 'Data Type', limit: 3 },
40
+ { file: 'ui-libraries.csv', column: 'Best For Tags', limit: 3 },
40
41
  ],
41
42
  zara: [
42
43
  { file: 'colors.csv', column: 'Product Type', limit: 3 },
@@ -44,6 +45,8 @@ var REFERENCE_MAP = {
44
45
  { file: 'styles.csv', column: 'Best For Tags', limit: 3 },
45
46
  { file: 'landing.csv', column: 'Keywords', limit: 3 },
46
47
  { file: 'icons.csv', column: 'Keywords', limit: 3 },
48
+ { file: 'motion.csv', column: 'Category', limit: 3 },
49
+ { file: 'ui-libraries.csv', column: 'Job', limit: 3 },
47
50
  ],
48
51
  meera: [
49
52
  { file: 'products.csv', column: 'Product Type', limit: 3 },
@@ -55,14 +58,17 @@ var REFERENCE_MAP = {
55
58
  { file: 'ui-reasoning.csv', column: 'UI Category', limit: 3 },
56
59
  { file: 'app-interface.csv', column: 'Keywords', limit: 3 },
57
60
  { file: 'icons.csv', column: 'Keywords', limit: 3 },
61
+ { file: 'ui-libraries.csv', column: 'Job', limit: 3 },
58
62
  ],
59
63
  anuj: [
60
64
  { file: 'ux-guidelines.csv', column: 'Category', limit: 3 },
61
65
  { file: 'app-interface.csv', column: 'Keywords', limit: 3 },
62
66
  { file: 'stacks/shadcn.csv', column: 'Category', limit: 3 },
67
+ { file: 'ui-libraries.csv', column: 'Job', limit: 3 },
63
68
  ],
64
69
  priya: [
65
70
  { file: 'react-performance.csv', column: 'Category', limit: 3 },
71
+ { file: 'ui-libraries.csv', column: 'Job', limit: 3 },
66
72
  ],
67
73
  raj: [
68
74
  { file: 'products.csv', column: 'Product Type', limit: 3 },
@@ -101,7 +107,9 @@ function getReferenceSpec(id, state) {
101
107
  if (!Array.isArray(specs)) specs = [specs];
102
108
  if (id === 'priya') {
103
109
  var stack = detectStack(state && state.vault_path);
104
- if (stack) return [{ file: 'stacks/' + stack + '.csv', column: 'Category', limit: 3 }];
110
+ if (stack) {
111
+ return [{ file: 'stacks/' + stack + '.csv', column: 'Category', limit: 3 }].concat(specs);
112
+ }
105
113
  }
106
114
  return specs;
107
115
  }
@@ -49,6 +49,7 @@ function defaultState(projectId) {
49
49
  },
50
50
  information_hierarchy: { declared_by: 'undeclared', ranking: [] },
51
51
  routing_decision: { problem_type: '', experts: [], reason: '' },
52
+ active_run: null,
52
53
  persona_outputs: {},
53
54
  full_prompts: {},
54
55
  structured_outputs: {},
@@ -64,6 +65,13 @@ function defaultState(projectId) {
64
65
  },
65
66
  vault_path: '',
66
67
  last_collect_at: '',
68
+ installed_ui_libraries: {
69
+ installed: [],
70
+ missing_defaults: [],
71
+ full_kits: [],
72
+ mixed_kits: false,
73
+ folders: [],
74
+ },
67
75
  deliberation: {
68
76
  round: 0,
69
77
  max_rounds: 3,
@@ -214,11 +222,22 @@ function ensurePersonaOutput({ project, persona, text } = {}) {
214
222
  const outputs = { ...(state.persona_outputs || {}) };
215
223
  let created = false;
216
224
  if (!outputs[persona]) {
225
+ if (state.active_run && !text) {
226
+ return { projectId, state, created: false, stale: true };
227
+ }
228
+ const now = new Date().toISOString();
217
229
  outputs[persona] = {
218
230
  text: text || '(slash — designer marked this note; full text was in chat)',
219
- at: new Date().toISOString(),
231
+ at: now,
220
232
  accepted: null,
221
233
  source: 'slash',
234
+ run_id: state.active_run?.id || `slash-${Date.now()}`,
235
+ attribution: {
236
+ provider: 'host',
237
+ model: 'unknown',
238
+ model_id: 'host/unknown',
239
+ effort: 'unknown',
240
+ },
222
241
  };
223
242
  created = true;
224
243
  state = update({ project: projectId, patch: { persona_outputs: outputs } });
@@ -233,7 +252,11 @@ function markAccepted({ project, persona, accepted = true } = {}) {
233
252
  if (!state || !state.persona_outputs || !state.persona_outputs[persona]) {
234
253
  return { updated: false, reason: `No output recorded for persona "${persona}" in this session yet.` };
235
254
  }
236
- state.persona_outputs[persona].accepted = accepted;
255
+ const output = state.persona_outputs[persona];
256
+ if (state.active_run && output.run_id !== state.active_run.id) {
257
+ return { updated: false, reason: `The "${persona}" output is from an older run. Run the persona again before rating it.` };
258
+ }
259
+ output.accepted = accepted;
237
260
  const merged = update({ project: projectId, patch: { persona_outputs: state.persona_outputs } });
238
261
  return { updated: true, state: merged };
239
262
  }
@@ -246,8 +269,10 @@ function logHostTurn({ project, path: turnPath, persona, inferred_in, verdicts =
246
269
  const existing = show({ project: projectId }) || defaultState(projectId);
247
270
  const metrics = { ...(existing.metrics || {}) };
248
271
  const turns = Array.isArray(metrics.host_turns) ? metrics.host_turns.slice() : [];
272
+ const now = new Date().toISOString();
273
+ const runId = `host-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
249
274
  turns.push({
250
- at: new Date().toISOString(),
275
+ at: now,
251
276
  path: turnPath || 'mcp',
252
277
  persona: persona || '',
253
278
  inferred_in: Number(inferred_in) || 0,
@@ -259,7 +284,26 @@ function logHostTurn({ project, path: turnPath, persona, inferred_in, verdicts =
259
284
  const cliIn = metrics.input_tokens_est || 0;
260
285
  const v = metrics.verdicts_this_run || (metrics.experts_run && metrics.experts_run.length ? 1 : 0) || (metrics.host_turns.length ? 1 : 0);
261
286
  metrics.tokens_per_verdict = v && (host + cliIn) ? Math.round((host + cliIn) / v) : null;
262
- return update({ project: projectId, patch: { metrics } });
287
+ const patch = { metrics };
288
+ if (persona && PERSONA_IDS.indexOf(persona) !== -1) {
289
+ patch.active_run = { id: runId, mode: 'host', at: now };
290
+ patch.persona_outputs = {
291
+ [persona]: {
292
+ text: '(host chat — full response remains in the IDE conversation)',
293
+ at: now,
294
+ accepted: null,
295
+ source: turnPath || 'mcp',
296
+ run_id: runId,
297
+ attribution: {
298
+ provider: 'host',
299
+ model: 'unknown',
300
+ model_id: 'host/unknown',
301
+ effort: 'unknown',
302
+ },
303
+ },
304
+ };
305
+ }
306
+ return update({ project: projectId, patch });
263
307
  }
264
308
 
265
309
  module.exports = {