analyzthis_design 2.0.0 → 2.1.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 +436 -0
- package/README.md +29 -13
- package/agents/cards/evolve-check.md +38 -0
- package/agents/manifests/evolve-check.json +16 -0
- package/dist/HOW-TO-USE.md +15 -3
- package/dist/README.md +29 -13
- package/dist/agents/cards/evolve-check.md +38 -0
- package/dist/agents/manifests/evolve-check.json +16 -0
- package/dist/bin/cli.js +1225 -1
- package/dist/lib/cache.js +111 -1
- package/dist/lib/chunk-executor.js +219 -1
- package/dist/lib/chunk-models.js +228 -1
- package/dist/lib/chunk-planner.js +328 -1
- package/dist/lib/chunk-router.js +66 -1
- package/dist/lib/chunk-run.js +199 -1
- package/dist/lib/chunk-synthesis.js +176 -1
- package/dist/lib/chunk-telemetry.js +88 -1
- package/dist/lib/collect.js +858 -1
- package/dist/lib/cost.js +119 -1
- package/dist/lib/dedup.js +167 -1
- package/dist/lib/deliberation.js +721 -1
- package/dist/lib/design-spec.js +236 -1
- package/dist/lib/evolution-metrics.js +197 -0
- package/dist/lib/evolve.js +361 -1
- package/dist/lib/export.js +77 -1
- package/dist/lib/feedback-submit.js +324 -1
- package/dist/lib/feedback.js +182 -1
- package/dist/lib/host-llm.js +251 -1
- package/dist/lib/install.js +301 -1
- package/dist/lib/knowledge.js +384 -1
- package/dist/lib/lessons.js +217 -1
- package/dist/lib/moodboard.js +563 -1
- package/dist/lib/orchestrator/run.js +935 -1
- package/dist/lib/outcome.js +193 -1
- package/dist/lib/platforms.js +166 -1
- package/dist/lib/provider.js +57 -1
- package/dist/lib/query-expander.js +83 -1
- package/dist/lib/ranker.js +105 -1
- package/dist/lib/reference-pack.js +221 -0
- package/dist/lib/research.js +143 -1
- package/dist/lib/retrieve.js +131 -1
- package/dist/lib/session.js +185 -1
- package/dist/lib/source-discovery.js +486 -1
- package/dist/lib/synthesis.js +155 -1
- package/dist/lib/token-gate.js +46 -1
- package/dist/skills/design-reference/google-fonts.csv +1924 -1924
- package/dist/skills/design-reference/products.csv +162 -162
- package/dist/skills/design-reference/schema.json +159 -0
- package/dist/skills/design-reference/stacks/angular.csv +1 -1
- package/dist/skills/design-reference/stacks/astro.csv +1 -1
- package/dist/skills/design-reference/stacks/laravel.csv +2 -2
- package/dist/skills/design-reference/stacks/threejs.csv +54 -54
- package/dist/skills/design-reference/styles.csv +85 -85
- package/dist/skills/design-reference/typography.csv +75 -74
- package/dist/skills/design-reference/ui-reasoning.csv +1 -1
- package/dist/skills/evolve-check/SKILL.md +106 -0
- package/package.json +8 -8
- package/scripts/validate-csvs.js +197 -0
- package/skills/design-reference/google-fonts.csv +1924 -1924
- package/skills/design-reference/products.csv +162 -162
- package/skills/design-reference/schema.json +159 -0
- package/skills/design-reference/stacks/angular.csv +1 -1
- package/skills/design-reference/stacks/astro.csv +1 -1
- package/skills/design-reference/stacks/laravel.csv +2 -2
- package/skills/design-reference/stacks/threejs.csv +54 -54
- package/skills/design-reference/styles.csv +85 -85
- package/skills/design-reference/typography.csv +75 -74
- package/skills/design-reference/ui-reasoning.csv +1 -1
- package/skills/evolve-check/SKILL.md +106 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reference pack builder (v2.0).
|
|
5
|
+
*
|
|
6
|
+
* Extracted from lib/orchestrator/run.js so both the chunked path
|
|
7
|
+
* (chunk-executor) and the legacy unchunked path can share the same
|
|
8
|
+
* CSV + vault retrieval pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Builds a ranked, citation-ready reference pack for one persona:
|
|
11
|
+
* 1. Query expansion (LLM, cached) → broadened keywords
|
|
12
|
+
* 2. CSV retrieve (file I/O, cached by mtime) → filtered rows
|
|
13
|
+
* 3. Knowledge-bank slice read (cached) → per-persona vault notes
|
|
14
|
+
* 4. LLM ranker → top-5 citations
|
|
15
|
+
*
|
|
16
|
+
* Falls back to static keywords + unranked rows on any error.
|
|
17
|
+
*
|
|
18
|
+
* CommonJS, 'use strict', var.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
var fs = require('fs');
|
|
22
|
+
var path = require('path');
|
|
23
|
+
var retrieve = require('./retrieve');
|
|
24
|
+
var knowledge = require('./knowledge');
|
|
25
|
+
var queryExpander = require('./query-expander');
|
|
26
|
+
var ranker = require('./ranker');
|
|
27
|
+
|
|
28
|
+
var PRODUCT_KEYWORDS = [
|
|
29
|
+
'saas', 'b2b', 'b2c', 'dashboard', 'analytics', 'e-commerce', 'ecommerce',
|
|
30
|
+
'fintech', 'healthcare', 'crm', 'marketplace', 'admin', 'enterprise',
|
|
31
|
+
'consumer', 'mobile', 'productivity', 'social', 'education', 'finance',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
var REFERENCE_MAP = {
|
|
35
|
+
arjun: [
|
|
36
|
+
{ file: 'styles.csv', column: 'Best For Tags', limit: 3 },
|
|
37
|
+
{ file: 'ux-guidelines.csv', column: 'Category', limit: 3 },
|
|
38
|
+
{ file: 'ui-reasoning.csv', column: 'UI Category', limit: 3 },
|
|
39
|
+
{ file: 'charts.csv', column: 'Data Type', limit: 3 },
|
|
40
|
+
],
|
|
41
|
+
zara: [
|
|
42
|
+
{ file: 'colors.csv', column: 'Product Type', limit: 3 },
|
|
43
|
+
{ file: 'typography.csv', column: 'Best For Tags', limit: 3 },
|
|
44
|
+
{ file: 'styles.csv', column: 'Best For Tags', limit: 3 },
|
|
45
|
+
{ file: 'landing.csv', column: 'Keywords', limit: 3 },
|
|
46
|
+
{ file: 'icons.csv', column: 'Keywords', limit: 3 },
|
|
47
|
+
],
|
|
48
|
+
meera: [
|
|
49
|
+
{ file: 'products.csv', column: 'Product Type', limit: 3 },
|
|
50
|
+
{ file: 'ui-reasoning.csv', column: 'UI Category', limit: 3 },
|
|
51
|
+
{ file: 'landing.csv', column: 'Keywords', limit: 3 },
|
|
52
|
+
],
|
|
53
|
+
noor: [
|
|
54
|
+
{ file: 'ux-guidelines.csv', column: 'Category', limit: 3 },
|
|
55
|
+
{ file: 'ui-reasoning.csv', column: 'UI Category', limit: 3 },
|
|
56
|
+
{ file: 'app-interface.csv', column: 'Keywords', limit: 3 },
|
|
57
|
+
{ file: 'icons.csv', column: 'Keywords', limit: 3 },
|
|
58
|
+
],
|
|
59
|
+
anuj: [
|
|
60
|
+
{ file: 'ux-guidelines.csv', column: 'Category', limit: 3 },
|
|
61
|
+
{ file: 'app-interface.csv', column: 'Keywords', limit: 3 },
|
|
62
|
+
{ file: 'stacks/shadcn.csv', column: 'Category', limit: 3 },
|
|
63
|
+
],
|
|
64
|
+
priya: [
|
|
65
|
+
{ file: 'react-performance.csv', column: 'Category', limit: 3 },
|
|
66
|
+
],
|
|
67
|
+
raj: [
|
|
68
|
+
{ file: 'products.csv', column: 'Product Type', limit: 3 },
|
|
69
|
+
{ file: 'landing.csv', column: 'Keywords', limit: 3 },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function extractKeywords(text) {
|
|
74
|
+
var lower = (text || '').toLowerCase();
|
|
75
|
+
return PRODUCT_KEYWORDS.filter(function(k) { return lower.indexOf(k) !== -1; });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectStack(vaultPath) {
|
|
79
|
+
if (!vaultPath || !fs.existsSync(path.join(vaultPath, 'Tech'))) return null;
|
|
80
|
+
var techNotes = fs.readdirSync(path.join(vaultPath, 'Tech')).map(function(f) { return f.toLowerCase(); });
|
|
81
|
+
// Check all 16 stacks. Order matters for disambiguation:
|
|
82
|
+
// nuxt-ui must be checked before nuxtjs (nuxt-ui notes may contain "nuxt")
|
|
83
|
+
// shadcn must be checked before react (shadcn notes may contain "react")
|
|
84
|
+
// react-native must be checked before react
|
|
85
|
+
// jetpack-compose must be checked before other android
|
|
86
|
+
var stacks = [
|
|
87
|
+
'nuxt-ui', 'nuxtjs', 'nextjs', 'shadcn', 'react-native', 'react',
|
|
88
|
+
'vue', 'angular', 'svelte', 'astro', 'html-tailwind',
|
|
89
|
+
'flutter', 'swiftui', 'jetpack-compose', 'laravel', 'threejs',
|
|
90
|
+
];
|
|
91
|
+
for (var i = 0; i < stacks.length; i++) {
|
|
92
|
+
// Match if any tech note filename contains the stack name
|
|
93
|
+
if (techNotes.some(function(n) { return n.indexOf(stacks[i]) !== -1; })) return stacks[i];
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getReferenceSpec(id, state) {
|
|
99
|
+
var specs = REFERENCE_MAP[id];
|
|
100
|
+
if (!specs) return null;
|
|
101
|
+
if (!Array.isArray(specs)) specs = [specs];
|
|
102
|
+
if (id === 'priya') {
|
|
103
|
+
var stack = detectStack(state && state.vault_path);
|
|
104
|
+
if (stack) return [{ file: 'stacks/' + stack + '.csv', column: 'Category', limit: 3 }];
|
|
105
|
+
}
|
|
106
|
+
return specs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a ranked reference pack for one persona.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} id — persona id
|
|
113
|
+
* @param {string} task — task text
|
|
114
|
+
* @param {object} state — session state
|
|
115
|
+
* @param {function} [callLlmFn] — LLM caller (defaults to orchestrator.callLlm)
|
|
116
|
+
* @returns {Promise<{cacheHit: boolean, citations: string[]} | null>}
|
|
117
|
+
*/
|
|
118
|
+
async function buildReferencePack(id, task, state, callLlmFn) {
|
|
119
|
+
var specs = getReferenceSpec(id, state);
|
|
120
|
+
if (!specs || !specs.length) return null;
|
|
121
|
+
|
|
122
|
+
var keywords = extractKeywords(task);
|
|
123
|
+
|
|
124
|
+
if (!callLlmFn) {
|
|
125
|
+
try { callLlmFn = require('./orchestrator/run').callLlm; }
|
|
126
|
+
catch (e) { callLlmFn = null; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (callLlmFn) {
|
|
130
|
+
try {
|
|
131
|
+
var expanded = await queryExpander.expandQuery({
|
|
132
|
+
task: task, personaId: id,
|
|
133
|
+
callLlmFn: callLlmFn,
|
|
134
|
+
provider: 'host', model: 'devi',
|
|
135
|
+
});
|
|
136
|
+
if (expanded && expanded.terms && expanded.terms.length) {
|
|
137
|
+
keywords = expanded.terms;
|
|
138
|
+
}
|
|
139
|
+
} catch (e) { /* fall back to static keywords */ }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!keywords.length) return null;
|
|
143
|
+
|
|
144
|
+
// Pool rows from all specs (multi-file retrieval with shared ranker).
|
|
145
|
+
var pool = [];
|
|
146
|
+
var anyCacheHit = false;
|
|
147
|
+
|
|
148
|
+
for (var s = 0; s < specs.length; s++) {
|
|
149
|
+
var spec = specs[s];
|
|
150
|
+
try {
|
|
151
|
+
var result = retrieve.retrieve({
|
|
152
|
+
file: spec.file,
|
|
153
|
+
filters: [{ column: spec.column, anyOf: keywords }],
|
|
154
|
+
limit: 10,
|
|
155
|
+
});
|
|
156
|
+
if (result.cacheHit) anyCacheHit = true;
|
|
157
|
+
if (!result.rows.length) continue;
|
|
158
|
+
|
|
159
|
+
for (var i = 0; i < result.rows.length; i++) {
|
|
160
|
+
var row = result.rows[i];
|
|
161
|
+
pool.push({
|
|
162
|
+
type: 'reference',
|
|
163
|
+
source: spec.file,
|
|
164
|
+
row: row.__no,
|
|
165
|
+
content: row[spec.column] || '',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} catch (e) { /* file missing or unreadable — skip */ }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add knowledge-bank slice notes to the pool.
|
|
172
|
+
try {
|
|
173
|
+
var kbNotes = knowledge.getPersonaSliceForPrompt(state, id);
|
|
174
|
+
for (var j = 0; j < Math.min(kbNotes.length, 5); j++) {
|
|
175
|
+
pool.push({
|
|
176
|
+
type: 'knowledge',
|
|
177
|
+
source: kbNotes[j].title,
|
|
178
|
+
row: 0,
|
|
179
|
+
content: kbNotes[j].content.slice(0, 200),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
} catch (e) { /* knowledge slices unavailable */ }
|
|
183
|
+
|
|
184
|
+
if (!pool.length) return null;
|
|
185
|
+
|
|
186
|
+
// Single ranker call across the merged pool (same cost as before, better coverage).
|
|
187
|
+
if (callLlmFn) {
|
|
188
|
+
try {
|
|
189
|
+
var ranked = await ranker.rankCandidates({
|
|
190
|
+
personaId: id,
|
|
191
|
+
task: task,
|
|
192
|
+
candidates: pool,
|
|
193
|
+
callLlmFn: callLlmFn,
|
|
194
|
+
provider: 'host', model: 'devi',
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
cacheHit: anyCacheHit,
|
|
198
|
+
citations: ranked.ranked.slice(0, 5).map(function(c) {
|
|
199
|
+
return '[' + c.source + (c.row ? ', row ' + c.row : '') + ': "' + c.content + '"]';
|
|
200
|
+
}),
|
|
201
|
+
};
|
|
202
|
+
} catch (e) { /* fall back to unranked */ }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Fallback: first 5 from pool, unranked.
|
|
206
|
+
return {
|
|
207
|
+
cacheHit: anyCacheHit,
|
|
208
|
+
citations: pool.slice(0, 5).map(function(c) {
|
|
209
|
+
return '[' + c.source + (c.row ? ', row ' + c.row : '') + ': "' + c.content + '"]';
|
|
210
|
+
}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
buildReferencePack: buildReferencePack,
|
|
216
|
+
getReferenceSpec: getReferenceSpec,
|
|
217
|
+
extractKeywords: extractKeywords,
|
|
218
|
+
detectStack: detectStack,
|
|
219
|
+
REFERENCE_MAP: REFERENCE_MAP,
|
|
220
|
+
PRODUCT_KEYWORDS: PRODUCT_KEYWORDS,
|
|
221
|
+
};
|
package/dist/lib/research.js
CHANGED
|
@@ -1,2 +1,144 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const { getProjectId, sessionDir } = require('./session');
|
|
10
|
+
|
|
11
|
+
const CONFIG_FILE = path.join(os.homedir(), '.analyzthis_design', 'config.json');
|
|
12
|
+
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
15
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); }
|
|
16
|
+
catch { return {}; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Strip HTML tags to a readable text snippet (no heavy deps)
|
|
20
|
+
function htmlToText(html) {
|
|
21
|
+
return html
|
|
22
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
23
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
24
|
+
.replace(/<[^>]+>/g, ' ')
|
|
25
|
+
.replace(/ /g, ' ')
|
|
26
|
+
.replace(/&/g, '&')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/>/g, '>')
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/\s+/g, ' ')
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fetchUrl(url, redirectsLeft = 5) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const lib = url.startsWith('https') ? https : http;
|
|
37
|
+
const req = lib.get(url, { headers: { 'User-Agent': 'analyzthis_design/1.8' }, timeout: 15000 }, (res) => {
|
|
38
|
+
// Follow redirects
|
|
39
|
+
if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
|
|
40
|
+
const next = res.headers.location.startsWith('http')
|
|
41
|
+
? res.headers.location
|
|
42
|
+
: new URL(res.headers.location, url).href;
|
|
43
|
+
res.resume();
|
|
44
|
+
return resolve(fetchUrl(next, redirectsLeft - 1));
|
|
45
|
+
}
|
|
46
|
+
if (res.statusCode !== 200) {
|
|
47
|
+
res.resume();
|
|
48
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
49
|
+
}
|
|
50
|
+
const chunks = [];
|
|
51
|
+
res.on('data', (c) => chunks.push(c));
|
|
52
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
53
|
+
});
|
|
54
|
+
req.on('error', reject);
|
|
55
|
+
req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout fetching ${url}`)); });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function webContextPath(projectId) {
|
|
60
|
+
return path.join(sessionDir(projectId), 'web-context.md');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Append a research snippet to the session's web-context.md.
|
|
65
|
+
* Returns the absolute path written.
|
|
66
|
+
*/
|
|
67
|
+
function appendWebContext(projectId, { title, source, body }) {
|
|
68
|
+
const dir = sessionDir(projectId);
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
const filePath = webContextPath(projectId);
|
|
71
|
+
const date = new Date().toISOString();
|
|
72
|
+
const block = `\n## ${title}\n\n> Source: ${source}\n> Fetched: ${date}\n\n${body}\n\n---\n`;
|
|
73
|
+
if (!fs.existsSync(filePath)) {
|
|
74
|
+
fs.writeFileSync(filePath, `# Web Research Context\n\n> Project: ${projectId}\n\n---\n`);
|
|
75
|
+
}
|
|
76
|
+
fs.appendFileSync(filePath, block);
|
|
77
|
+
return filePath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch a URL and write a cleaned text snippet into web-context.md.
|
|
82
|
+
*/
|
|
83
|
+
async function researchUrl({ url, project, maxChars = 4000 } = {}) {
|
|
84
|
+
if (!url) throw new Error('--url is required');
|
|
85
|
+
const projectId = project || getProjectId();
|
|
86
|
+
const raw = await fetchUrl(url);
|
|
87
|
+
const text = htmlToText(raw).slice(0, maxChars);
|
|
88
|
+
const filePath = appendWebContext(projectId, {
|
|
89
|
+
title: `Fetched: ${url}`,
|
|
90
|
+
source: url,
|
|
91
|
+
body: text || '_No readable text extracted._',
|
|
92
|
+
});
|
|
93
|
+
return { projectId, filePath, chars: text.length };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Research by query. Uses research.provider from config when set;
|
|
98
|
+
* otherwise writes a stub prompting the host IDE to use WebSearch.
|
|
99
|
+
*/
|
|
100
|
+
async function researchQuery({ query, project, maxChars = 4000 } = {}) {
|
|
101
|
+
if (!query) throw new Error('--query is required');
|
|
102
|
+
const projectId = project || getProjectId();
|
|
103
|
+
const config = loadConfig();
|
|
104
|
+
const provider = (config.research && config.research.provider) || null;
|
|
105
|
+
|
|
106
|
+
// Optional: provider URL template, e.g. a custom search endpoint that returns HTML/JSON text
|
|
107
|
+
if (provider && typeof provider === 'string' && provider.startsWith('http')) {
|
|
108
|
+
const searchUrl = provider.replace('{query}', encodeURIComponent(query));
|
|
109
|
+
const raw = await fetchUrl(searchUrl);
|
|
110
|
+
const text = htmlToText(raw).slice(0, maxChars);
|
|
111
|
+
const filePath = appendWebContext(projectId, {
|
|
112
|
+
title: `Search: ${query}`,
|
|
113
|
+
source: searchUrl,
|
|
114
|
+
body: text || '_No readable text extracted._',
|
|
115
|
+
});
|
|
116
|
+
return { projectId, filePath, chars: text.length, mode: 'provider' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// No provider configured — write a stub the orchestrator / host IDE fills via WebSearch/WebFetch
|
|
120
|
+
const stub = [
|
|
121
|
+
`_No research.provider configured in ~/.analyzthis_design/config.json._`,
|
|
122
|
+
``,
|
|
123
|
+
`Host IDE: use WebSearch / WebFetch for query "${query}" and append the result here,`,
|
|
124
|
+
`or set config.research.provider to a search URL template containing {query}.`,
|
|
125
|
+
].join('\n');
|
|
126
|
+
const filePath = appendWebContext(projectId, {
|
|
127
|
+
title: `Search stub: ${query}`,
|
|
128
|
+
source: `query:${query}`,
|
|
129
|
+
body: stub,
|
|
130
|
+
});
|
|
131
|
+
return { projectId, filePath, chars: stub.length, mode: 'stub' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Read the current web-context.md for a project (or null if none).
|
|
136
|
+
*/
|
|
137
|
+
function readWebContext({ project } = {}) {
|
|
138
|
+
const projectId = project || getProjectId();
|
|
139
|
+
const filePath = webContextPath(projectId);
|
|
140
|
+
if (!fs.existsSync(filePath)) return null;
|
|
141
|
+
return { projectId, filePath, content: fs.readFileSync(filePath, 'utf8') };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { researchUrl, researchQuery, readWebContext, webContextPath, appendWebContext };
|
package/dist/lib/retrieve.js
CHANGED
|
@@ -1 +1,131 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retrieve-on-demand reference rows from skills/design-reference/*.csv.
|
|
5
|
+
*
|
|
6
|
+
* Personas do not need the full CSV — just the rows relevant to the active
|
|
7
|
+
* product type / stack / dimension. This module filters rows by keyword match
|
|
8
|
+
* on named columns and returns a compact, citation-ready pack, caching the
|
|
9
|
+
* result via lib/cache.js so repeat calls in the same run (or across runs,
|
|
10
|
+
* until the source file changes) don't re-parse and re-filter the CSV.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const cache = require('./cache');
|
|
16
|
+
|
|
17
|
+
const { resolvePackageRoot } = require('./platforms');
|
|
18
|
+
const PACKAGE_ROOT = resolvePackageRoot(__dirname);
|
|
19
|
+
|
|
20
|
+
const REFERENCE_DIR = path.join(PACKAGE_ROOT, 'skills', 'design-reference');
|
|
21
|
+
|
|
22
|
+
// ─── CSV parsing (handles quoted fields containing commas) ──────────────────
|
|
23
|
+
|
|
24
|
+
function parseCsvLine(line) {
|
|
25
|
+
const cells = [];
|
|
26
|
+
let cur = '';
|
|
27
|
+
let inQuotes = false;
|
|
28
|
+
for (let i = 0; i < line.length; i++) {
|
|
29
|
+
const ch = line[i];
|
|
30
|
+
if (inQuotes) {
|
|
31
|
+
if (ch === '"' && line[i + 1] === '"') { cur += '"'; i++; }
|
|
32
|
+
else if (ch === '"') { inQuotes = false; }
|
|
33
|
+
else { cur += ch; }
|
|
34
|
+
} else if (ch === '"') {
|
|
35
|
+
inQuotes = true;
|
|
36
|
+
} else if (ch === ',') {
|
|
37
|
+
cells.push(cur);
|
|
38
|
+
cur = '';
|
|
39
|
+
} else {
|
|
40
|
+
cur += ch;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
cells.push(cur);
|
|
44
|
+
return cells;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Split raw CSV text into logical rows, respecting quoted newlines.
|
|
49
|
+
*/
|
|
50
|
+
function splitCsvRows(text) {
|
|
51
|
+
const rows = [];
|
|
52
|
+
let cur = '';
|
|
53
|
+
let inQuotes = false;
|
|
54
|
+
for (let i = 0; i < text.length; i++) {
|
|
55
|
+
const ch = text[i];
|
|
56
|
+
if (ch === '"') inQuotes = !inQuotes;
|
|
57
|
+
if (ch === '\n' && !inQuotes) {
|
|
58
|
+
rows.push(cur);
|
|
59
|
+
cur = '';
|
|
60
|
+
} else if (ch !== '\r') {
|
|
61
|
+
cur += ch;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (cur.trim().length) rows.push(cur);
|
|
65
|
+
return rows;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadCsv(relPath) {
|
|
69
|
+
const filePath = path.join(REFERENCE_DIR, relPath);
|
|
70
|
+
if (!fs.existsSync(filePath)) throw new Error(`Reference file not found: ${relPath}`);
|
|
71
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
72
|
+
const mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
73
|
+
const lines = splitCsvRows(text);
|
|
74
|
+
const header = parseCsvLine(lines[0]);
|
|
75
|
+
const rows = lines.slice(1).map((line, i) => {
|
|
76
|
+
const cells = parseCsvLine(line);
|
|
77
|
+
const obj = {};
|
|
78
|
+
header.forEach((h, idx) => { obj[h.trim()] = (cells[idx] || '').trim(); });
|
|
79
|
+
// Prefer the file's own "No" column as the citation row number (matches
|
|
80
|
+
// the convention already used across all persona SKILL.md examples);
|
|
81
|
+
// fall back to physical position if the column is absent.
|
|
82
|
+
obj.__no = obj.No || String(i + 1);
|
|
83
|
+
obj.__line = i + 2; // +1 for header, +1 for 1-indexing
|
|
84
|
+
return obj;
|
|
85
|
+
});
|
|
86
|
+
return { header, rows, mtimeMs };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Filtering ───────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {Array<object>} rows
|
|
93
|
+
* @param {Array<{ column: string, anyOf: string[] }>} filters - AND across
|
|
94
|
+
* filters, OR (substring, case-insensitive) within each filter's anyOf list.
|
|
95
|
+
*/
|
|
96
|
+
function filterRows(rows, filters = []) {
|
|
97
|
+
if (!filters.length) return rows;
|
|
98
|
+
return rows.filter((row) =>
|
|
99
|
+
filters.every((f) => {
|
|
100
|
+
const cell = (row[f.column] || '').toLowerCase();
|
|
101
|
+
return (f.anyOf || []).some((needle) => cell.includes(String(needle).toLowerCase()));
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Retrieve the top-N rows from a design-reference CSV matching `filters`,
|
|
107
|
+
* using the on-disk cache keyed by file + filters + the file's own mtime (so
|
|
108
|
+
* edits to the CSV invalidate stale cache entries automatically).
|
|
109
|
+
*
|
|
110
|
+
* @param {{ file: string, filters?: Array<{column:string, anyOf:string[]}>, limit?: number }} opts
|
|
111
|
+
* @returns {{ file: string, rows: object[], matched: number, cacheHit: boolean }}
|
|
112
|
+
*/
|
|
113
|
+
function retrieve({ file, filters = [], limit = 3 }) {
|
|
114
|
+
const { rows, mtimeMs } = loadCsv(file);
|
|
115
|
+
const cacheKey = `retrieve:${file}:${cache.hashFilter({ filters, limit, mtimeMs })}`;
|
|
116
|
+
const { value, hit } = cache.getOrCompute(cacheKey, () => {
|
|
117
|
+
const matched = filterRows(rows, filters);
|
|
118
|
+
return { file, rows: matched.slice(0, limit), matched: matched.length };
|
|
119
|
+
});
|
|
120
|
+
return { ...value, cacheHit: hit };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format a citation line for one retrieved row, in the mandatory format used
|
|
125
|
+
* by every persona: `[filename, row N: "exact quoted value"]`.
|
|
126
|
+
*/
|
|
127
|
+
function cite(file, row, column) {
|
|
128
|
+
return `[${file}, row ${row.__no}: "${row[column]}"]`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { REFERENCE_DIR, loadCsv, filterRows, retrieve, cite };
|