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.
Files changed (100) hide show
  1. package/HOW-TO-USE.md +249 -326
  2. package/README.md +71 -25
  3. package/agents/calibration-benchmark.json +187 -0
  4. package/agents/cards/anuj.md +1 -1
  5. package/agents/cards/arjun.md +1 -1
  6. package/agents/cards/kavi.md +2 -1
  7. package/agents/cards/mood-board.md +7 -7
  8. package/agents/cards/noor.md +1 -1
  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 +13 -8
  14. package/agents/model-policy.json +12 -0
  15. package/agents/router.json +2 -2
  16. package/agents/session-schema.json +12 -2
  17. package/dist/HOW-TO-USE.md +249 -326
  18. package/dist/README.md +71 -25
  19. package/dist/agents/calibration-benchmark.json +187 -0
  20. package/dist/agents/cards/anuj.md +1 -1
  21. package/dist/agents/cards/arjun.md +1 -1
  22. package/dist/agents/cards/kavi.md +2 -1
  23. package/dist/agents/cards/mood-board.md +7 -7
  24. package/dist/agents/cards/noor.md +1 -1
  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 +13 -8
  30. package/dist/agents/model-policy.json +12 -0
  31. package/dist/agents/router.json +2 -2
  32. package/dist/agents/session-schema.json +12 -2
  33. package/dist/bin/cli.js +308 -10
  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/install.js +1 -0
  48. package/dist/lib/lessons.js +8 -0
  49. package/dist/lib/mcp-server.js +189 -7
  50. package/dist/lib/model-policy.js +174 -0
  51. package/dist/lib/model-registry.js +71 -0
  52. package/dist/lib/moodboard-film.js +119 -0
  53. package/dist/lib/moodboard-view.js +426 -0
  54. package/dist/lib/moodboard.js +638 -27
  55. package/dist/lib/orchestrator/run.js +67 -8
  56. package/dist/lib/outcome.js +7 -2
  57. package/dist/lib/output-quality.js +95 -0
  58. package/dist/lib/reference-pack.js +9 -1
  59. package/dist/lib/session.js +48 -4
  60. package/dist/lib/system-prompt.js +3 -0
  61. package/dist/lib/ui-libraries.js +232 -0
  62. package/dist/skills/anuj/SKILL.md +4 -0
  63. package/dist/skills/arjun/SKILL.md +2 -0
  64. package/dist/skills/arjun/references/lens.md +2 -0
  65. package/dist/skills/design-director/SKILL.md +2 -1
  66. package/dist/skills/design-reference/SKILL.md +6 -0
  67. package/dist/skills/design-reference/motion.csv +11 -0
  68. package/dist/skills/design-reference/schema.json +12 -0
  69. package/dist/skills/design-reference/stacks/shadcn.csv +6 -0
  70. package/dist/skills/design-reference/ui-libraries.csv +46 -0
  71. package/dist/skills/design-spec/SKILL.md +1 -1
  72. package/dist/skills/getting-started/SKILL.md +2 -2
  73. package/dist/skills/kavi/SKILL.md +3 -2
  74. package/dist/skills/meera/SKILL.md +2 -0
  75. package/dist/skills/mood-board/SKILL.md +94 -87
  76. package/dist/skills/noor/SKILL.md +4 -0
  77. package/dist/skills/persona-orchestrator/references/lens.md +4 -4
  78. package/dist/skills/priya/SKILL.md +5 -0
  79. package/dist/skills/raj/SKILL.md +2 -0
  80. package/dist/skills/zara/SKILL.md +49 -4
  81. package/package.json +13 -2
  82. package/skills/anuj/SKILL.md +4 -0
  83. package/skills/arjun/SKILL.md +2 -0
  84. package/skills/arjun/references/lens.md +2 -0
  85. package/skills/design-director/SKILL.md +2 -1
  86. package/skills/design-reference/SKILL.md +6 -0
  87. package/skills/design-reference/motion.csv +11 -0
  88. package/skills/design-reference/schema.json +12 -0
  89. package/skills/design-reference/stacks/shadcn.csv +6 -0
  90. package/skills/design-reference/ui-libraries.csv +46 -0
  91. package/skills/design-spec/SKILL.md +1 -1
  92. package/skills/getting-started/SKILL.md +2 -2
  93. package/skills/kavi/SKILL.md +3 -2
  94. package/skills/meera/SKILL.md +2 -0
  95. package/skills/mood-board/SKILL.md +94 -87
  96. package/skills/noor/SKILL.md +4 -0
  97. package/skills/persona-orchestrator/references/lens.md +4 -4
  98. package/skills/priya/SKILL.md +5 -0
  99. package/skills/raj/SKILL.md +2 -0
  100. package/skills/zara/SKILL.md +49 -4
@@ -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 = {
@@ -32,8 +32,11 @@ function buildPrompt(opts) {
32
32
  lines.push('');
33
33
  lines.push('**CRITIQUE** — Review an existing screen. Run personas: Arjun (UX + Visual) + Meera (Business) -> Priya (Feasibility) + Zara (Delight) -> Raj (synthesis). Output: composite score, SHIP/REVISE/BLOCK verdict, top 3 fixes.');
34
34
  lines.push('');
35
+ lines.push('**DIRECTION** — Mood board (tagged URLs) or UI image board. For a UI board, Zara scouts 3-5 real shipped screens (never invent URLs, never generate AI comps). Arjun, Noor, Priya, Meera jury. She re-scouts if they skip (max 3 rounds).');
36
+ lines.push('');
35
37
  lines.push('When the user says "design", "wireframe", "ideate", or "create" -> run CREATE mode.');
36
38
  lines.push('When the user says "review", "critique", "audit", or "fix" -> run CRITIQUE mode.');
39
+ lines.push('When the user says "mood board", "UI board", "image board", or "visual direction" -> run DIRECTION mode.');
37
40
  lines.push('');
38
41
 
39
42
  lines.push('## Premise challenge (critical)');
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Detect installed motion / UX / visual-design libraries from package.json
5
+ * and copy-paste kit folders. Personas read Tech/Installed-UI-Libraries.md
6
+ * and session.installed_ui_libraries instead of guessing the stack.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const retrieve = require('./retrieve');
12
+
13
+ const DEFAULT_BY_JOB = {
14
+ primitives: 'Shadcn UI',
15
+ motion: 'Motion',
16
+ icons: 'Lucide',
17
+ };
18
+
19
+ const FOLDER_CANDIDATES = [
20
+ 'components/ui',
21
+ 'src/components/ui',
22
+ 'app/components/ui',
23
+ 'components/magicui',
24
+ 'src/components/magicui',
25
+ 'components/aceternity',
26
+ 'src/components/aceternity',
27
+ ];
28
+
29
+ function catalogRows() {
30
+ try {
31
+ return retrieve.loadCsv('ui-libraries.csv').rows || [];
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ function splitAliases(field) {
38
+ return String(field || '')
39
+ .split(';')
40
+ .map((s) => s.trim())
41
+ .filter(Boolean);
42
+ }
43
+
44
+ function npmMatches(depName, npmField) {
45
+ return splitAliases(npmField).some((alias) => {
46
+ if (alias.endsWith('/*')) {
47
+ const prefix = alias.slice(0, -1);
48
+ const pkg = alias.slice(0, -2);
49
+ return depName === pkg || depName.startsWith(prefix);
50
+ }
51
+ return depName === alias;
52
+ });
53
+ }
54
+
55
+ function readPackageDeps(cwd) {
56
+ const pkgPath = path.join(cwd, 'package.json');
57
+ if (!fs.existsSync(pkgPath)) return { deps: [], pkgPath: null };
58
+ try {
59
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
60
+ const names = Object.keys({
61
+ ...(pkg.dependencies || {}),
62
+ ...(pkg.devDependencies || {}),
63
+ });
64
+ return { deps: names, pkgPath };
65
+ } catch {
66
+ return { deps: [], pkgPath };
67
+ }
68
+ }
69
+
70
+ function folderExists(cwd, rel) {
71
+ try {
72
+ return fs.statSync(path.join(cwd, rel)).isDirectory();
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function detectFolders(cwd) {
79
+ const found = [];
80
+ for (const rel of FOLDER_CANDIDATES) {
81
+ if (folderExists(cwd, rel)) found.push(rel);
82
+ }
83
+ return found;
84
+ }
85
+
86
+ function folderHitsRow(foundFolders, detectionFolders) {
87
+ const needles = splitAliases(detectionFolders).filter(Boolean);
88
+ if (!needles.length) return false;
89
+ return foundFolders.some((rel) =>
90
+ needles.some((n) => rel === n || rel.endsWith('/' + n) || rel.indexOf(n) !== -1));
91
+ }
92
+
93
+ /**
94
+ * @param {{ cwd: string }} opts
95
+ * @returns {{ installed: object[], missing_defaults: object[], folders: string[], full_kits: string[], mixed_kits: boolean }}
96
+ */
97
+ function detectInstalledUiLibraries({ cwd } = {}) {
98
+ const root = path.resolve(cwd || process.cwd());
99
+ const { deps } = readPackageDeps(root);
100
+ const folders = detectFolders(root);
101
+ const rows = catalogRows();
102
+ const installed = [];
103
+ const seen = new Set();
104
+
105
+ for (const row of rows) {
106
+ const byNpm = deps.some((d) => npmMatches(d, row['Npm Names']));
107
+ const byFolder = folderHitsRow(folders, row['Detection Folders']);
108
+ if (!byNpm && !byFolder) continue;
109
+ const name = row.Library;
110
+ if (seen.has(name)) continue;
111
+ seen.add(name);
112
+ installed.push({
113
+ library: name,
114
+ job: row.Job,
115
+ persona: row.Persona,
116
+ import_example: row['Import Example'],
117
+ docs: row['Docs URL'],
118
+ do_not_use_for: row['Do Not Use For'],
119
+ bundle_hint: row['Bundle Hint'],
120
+ how: byNpm ? (byFolder ? 'package+folder' : 'package.json') : 'folder',
121
+ });
122
+ }
123
+
124
+ const jobsPresent = new Set(installed.map((i) => i.job));
125
+ const missing_defaults = [];
126
+ Object.keys(DEFAULT_BY_JOB).forEach((job) => {
127
+ if (jobsPresent.has(job)) return;
128
+ // full-kit covers primitives
129
+ if (job === 'primitives' && jobsPresent.has('full-kit')) return;
130
+ const libName = DEFAULT_BY_JOB[job];
131
+ const row = rows.find((r) => r.Library === libName);
132
+ missing_defaults.push({
133
+ job,
134
+ library: libName,
135
+ install: row ? row.Install : '',
136
+ docs: row ? row['Docs URL'] : '',
137
+ });
138
+ });
139
+
140
+ const full_kits = installed.filter((i) => i.job === 'full-kit').map((i) => i.library);
141
+ const hasShadcn = installed.some((i) => i.library === 'Shadcn UI' || i.library === 'Radix UI');
142
+ const mixed_kits = full_kits.length > 1 || (full_kits.length > 0 && hasShadcn);
143
+
144
+ return { installed, missing_defaults, folders, full_kits, mixed_kits };
145
+ }
146
+
147
+ function buildNoteMarkdown(detection) {
148
+ const date = new Date().toISOString().split('T')[0];
149
+ const installed = detection.installed || [];
150
+ const missing = detection.missing_defaults || [];
151
+ const lines = [
152
+ '---',
153
+ 'title: Installed-UI-Libraries',
154
+ 'tags: [tech, ui-libraries]',
155
+ 'source: package.json',
156
+ 'category: tech',
157
+ `collected_at: "${date}"`,
158
+ 'enriched: true',
159
+ '---',
160
+ '',
161
+ '## Purpose',
162
+ 'Libraries already in this repo that design personas must prefer. One library per job. Do not add a second full UI kit. Do not put marketing FX (Magic UI, Aceternity, 21st.dev, Lottie, Spline) on daily working dashboards.',
163
+ '',
164
+ '## Installed',
165
+ ];
166
+ if (!installed.length) {
167
+ lines.push('- _(none of the catalogued packages detected)_');
168
+ } else {
169
+ for (const i of installed) {
170
+ lines.push(`- **${i.library}** (${i.job}) via ${i.how} — \`${i.import_example}\``);
171
+ }
172
+ }
173
+ lines.push('', '## Missing defaults (optional — do not install unless the brief needs them)');
174
+ if (!missing.length) {
175
+ lines.push('- All default jobs covered.');
176
+ } else {
177
+ for (const m of missing) {
178
+ lines.push(`- **${m.job}:** ${m.library} — \`${m.install}\``);
179
+ }
180
+ }
181
+ lines.push('', '## Kit rule');
182
+ if (detection.mixed_kits) {
183
+ lines.push('- **BLOCK mix:** more than one full kit (or a full kit plus Shadcn) is present. Priya: do not add more. Use the incumbent kit.');
184
+ } else if (detection.full_kits && detection.full_kits.length) {
185
+ lines.push(`- Incumbent full kit: **${detection.full_kits.join(', ')}**. Prescribe this kit's components; do not start a Shadcn tree.`);
186
+ } else {
187
+ lines.push('- No full kit detected. Shadcn + Radix is the greenfield default for Tailwind apps.');
188
+ }
189
+ lines.push('', '## Folders found');
190
+ if (!(detection.folders || []).length) {
191
+ lines.push('- _(none)_');
192
+ } else {
193
+ for (const f of detection.folders) lines.push(`- \`${f}\``);
194
+ }
195
+ lines.push('');
196
+ return lines.join('\n');
197
+ }
198
+
199
+ function writeInstalledUiLibrariesNote({ vaultPath, detection, dryRun }) {
200
+ const md = buildNoteMarkdown(detection);
201
+ const rel = path.join('Tech', 'Installed-UI-Libraries.md');
202
+ if (dryRun || !vaultPath) {
203
+ return { vaultRel: rel, written: false, markdown: md };
204
+ }
205
+ const destDir = path.join(vaultPath, 'Tech');
206
+ fs.mkdirSync(destDir, { recursive: true });
207
+ const dest = path.join(destDir, 'Installed-UI-Libraries.md');
208
+ fs.writeFileSync(dest, md);
209
+ return { vaultRel: rel, written: true, markdown: md, dest };
210
+ }
211
+
212
+ function sessionPayload(detection) {
213
+ return {
214
+ installed: (detection.installed || []).map((i) => ({
215
+ library: i.library,
216
+ job: i.job,
217
+ how: i.how,
218
+ })),
219
+ missing_defaults: detection.missing_defaults || [],
220
+ full_kits: detection.full_kits || [],
221
+ mixed_kits: !!detection.mixed_kits,
222
+ folders: detection.folders || [],
223
+ };
224
+ }
225
+
226
+ module.exports = {
227
+ detectInstalledUiLibraries,
228
+ writeInstalledUiLibrariesNote,
229
+ buildNoteMarkdown,
230
+ sessionPayload,
231
+ DEFAULT_BY_JOB,
232
+ };