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
|
@@ -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
|
-
|
|
244
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
};
|
package/dist/lib/outcome.js
CHANGED
|
@@ -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)
|
|
110
|
+
if (stack) {
|
|
111
|
+
return [{ file: 'stacks/' + stack + '.csv', column: 'Category', limit: 3 }].concat(specs);
|
|
112
|
+
}
|
|
105
113
|
}
|
|
106
114
|
return specs;
|
|
107
115
|
}
|
package/dist/lib/session.js
CHANGED
|
@@ -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:
|
|
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]
|
|
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:
|
|
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
|
-
|
|
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
|
+
};
|