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
|
@@ -1 +1,324 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in anonymous submission of persona feedback to a central store (Supabase REST).
|
|
5
|
+
* Users must consent once; payloads are redacted before upload.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const readline = require('readline');
|
|
14
|
+
const session = require('./session');
|
|
15
|
+
const { listFeedback } = require('./feedback');
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.analyzthis_design');
|
|
18
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
19
|
+
const CONSENT_FILE = path.join(CONFIG_DIR, 'feedback', 'submit-consent.json');
|
|
20
|
+
|
|
21
|
+
const MAX_SUBMIT_TEXT = 2000;
|
|
22
|
+
const PATH_PATTERN = /(?:\/Users\/|\/home\/|[A-Za-z]:\\)[^\s"'`,;)]+/g;
|
|
23
|
+
const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
24
|
+
const SECRET_PATTERN = /\b(sk-[a-zA-Z0-9_-]{10,}|api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{8,})/gi;
|
|
25
|
+
|
|
26
|
+
function loadConfig() {
|
|
27
|
+
if (!fs.existsSync(CONFIG_FILE)) return { sources: [] };
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return { sources: [] };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function saveConfig(config) {
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
37
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveSubmitConfig() {
|
|
41
|
+
const config = loadConfig();
|
|
42
|
+
const fb = config.feedback || {};
|
|
43
|
+
const pkgVersion = safePackageVersion();
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
url: process.env.ANALYZTHIS_FEEDBACK_URL || fb.submit_url || '',
|
|
47
|
+
anonKey: process.env.ANALYZTHIS_FEEDBACK_ANON_KEY || fb.anon_key || '',
|
|
48
|
+
enabled: fb.submit_enabled !== false,
|
|
49
|
+
packageVersion: pkgVersion,
|
|
50
|
+
installId: getInstallId(config),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safePackageVersion() {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
57
|
+
return pkg.version || 'unknown';
|
|
58
|
+
} catch {
|
|
59
|
+
return 'unknown';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getInstallId(configIn) {
|
|
64
|
+
const config = configIn || loadConfig();
|
|
65
|
+
if (!config.feedback) config.feedback = {};
|
|
66
|
+
if (!config.feedback.install_id) {
|
|
67
|
+
config.feedback.install_id = crypto.randomBytes(16).toString('hex');
|
|
68
|
+
saveConfig(config);
|
|
69
|
+
}
|
|
70
|
+
return config.feedback.install_id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadConsent() {
|
|
74
|
+
if (!fs.existsSync(CONSENT_FILE)) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(CONSENT_FILE, 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function saveConsent() {
|
|
83
|
+
fs.mkdirSync(path.dirname(CONSENT_FILE), { recursive: true });
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
CONSENT_FILE,
|
|
86
|
+
JSON.stringify({ opted_in: true, at: new Date().toISOString(), version: 1 }, null, 2),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function revokeConsent() {
|
|
91
|
+
if (fs.existsSync(CONSENT_FILE)) fs.unlinkSync(CONSENT_FILE);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Redact paths, emails, secrets; truncate long text.
|
|
96
|
+
*/
|
|
97
|
+
function anonymizeText(text, maxLen = MAX_SUBMIT_TEXT) {
|
|
98
|
+
if (!text || typeof text !== 'string') return '';
|
|
99
|
+
const home = os.homedir();
|
|
100
|
+
let out = text.split(home).join('~/');
|
|
101
|
+
out = out.replace(PATH_PATTERN, '[path]');
|
|
102
|
+
out = out.replace(EMAIL_PATTERN, '[email]');
|
|
103
|
+
out = out.replace(SECRET_PATTERN, '[redacted]');
|
|
104
|
+
if (out.length > maxLen) out = out.slice(0, maxLen) + '…';
|
|
105
|
+
return out.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function entryToSubmitPayload(entry, cfg) {
|
|
109
|
+
return {
|
|
110
|
+
install_id: cfg.installId,
|
|
111
|
+
package_version: cfg.packageVersion,
|
|
112
|
+
feedback_id: entry.id,
|
|
113
|
+
persona: entry.persona,
|
|
114
|
+
satisfied: !!entry.satisfied,
|
|
115
|
+
rating: entry.rating,
|
|
116
|
+
tags: entry.tags || [],
|
|
117
|
+
user_comment: anonymizeText(entry.comment, 800),
|
|
118
|
+
assistant_rejected: anonymizeText(entry.original_output),
|
|
119
|
+
assistant_preferred: anonymizeText(entry.correction || entry.comment, 1200),
|
|
120
|
+
task_summary: anonymizeText(entry.context?.task_map_summary || '', 600),
|
|
121
|
+
problem_type: entry.context?.problem_type || '',
|
|
122
|
+
mode: entry.context?.mode || '',
|
|
123
|
+
recorded_at: entry.at,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectUnsentEntries({ project, all = false, persona, includePositive = false } = {}) {
|
|
128
|
+
let entries = listFeedback({ project, all });
|
|
129
|
+
if (persona) entries = entries.filter((e) => e.persona === persona);
|
|
130
|
+
if (!includePositive) entries = entries.filter((e) => !e.satisfied);
|
|
131
|
+
return entries.filter((e) => !e.submitted_at && (e.comment || e.correction));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function markEntriesSubmitted(entryIds) {
|
|
135
|
+
const idSet = new Set(entryIds);
|
|
136
|
+
let marked = 0;
|
|
137
|
+
|
|
138
|
+
for (const projectId of session.listProjects()) {
|
|
139
|
+
const state = session.show({ project: projectId });
|
|
140
|
+
if (!state?.feedback_log?.length) continue;
|
|
141
|
+
|
|
142
|
+
let changed = false;
|
|
143
|
+
const feedback_log = state.feedback_log.map((e) => {
|
|
144
|
+
if (!idSet.has(e.id) || e.submitted_at) return e;
|
|
145
|
+
changed = true;
|
|
146
|
+
marked += 1;
|
|
147
|
+
return { ...e, submitted_at: new Date().toISOString() };
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (changed) {
|
|
151
|
+
session.update({ project: projectId, patch: { feedback_log } });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return marked;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function postJson(url, headers, body) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const u = new URL(url);
|
|
161
|
+
const data = JSON.stringify(body);
|
|
162
|
+
const req = https.request(
|
|
163
|
+
{
|
|
164
|
+
hostname: u.hostname,
|
|
165
|
+
port: u.port || 443,
|
|
166
|
+
path: u.pathname + u.search,
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
'Content-Length': Buffer.byteLength(data),
|
|
171
|
+
...headers,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
(res) => {
|
|
175
|
+
let chunks = '';
|
|
176
|
+
res.on('data', (c) => { chunks += c; });
|
|
177
|
+
res.on('end', () => {
|
|
178
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
179
|
+
resolve({ status: res.statusCode, body: chunks });
|
|
180
|
+
} else {
|
|
181
|
+
reject(new Error(`Submit failed (${res.statusCode}): ${chunks.slice(0, 300)}`));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
req.on('error', reject);
|
|
187
|
+
req.write(data);
|
|
188
|
+
req.end();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function submitRows(rows, cfg) {
|
|
193
|
+
if (!cfg.url) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
'No feedback submit URL configured.\n'
|
|
196
|
+
+ ' Maintainer: set feedback.submit_url + feedback.anon_key in ~/.analyzthis_design/config.json\n'
|
|
197
|
+
+ ' Or env: ANALYZTHIS_FEEDBACK_URL and ANALYZTHIS_FEEDBACK_ANON_KEY\n'
|
|
198
|
+
+ ' See README → "Community feedback collection"',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!cfg.anonKey) {
|
|
202
|
+
throw new Error('Missing anon key. Set feedback.anon_key in config or ANALYZTHIS_FEEDBACK_ANON_KEY.');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await postJson(cfg.url, {
|
|
206
|
+
apikey: cfg.anonKey,
|
|
207
|
+
Authorization: `Bearer ${cfg.anonKey}`,
|
|
208
|
+
Prefer: 'return=minimal',
|
|
209
|
+
}, rows);
|
|
210
|
+
|
|
211
|
+
return rows.length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function askConsentQuestion() {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
217
|
+
rl.question(
|
|
218
|
+
'\nShare anonymized persona feedback with analyzthis_design maintainers?\n'
|
|
219
|
+
+ ' Sends: persona, rating, tags, comment/correction, redacted output snippets\n'
|
|
220
|
+
+ ' Does NOT send: project paths, repo names, emails, or API keys\n'
|
|
221
|
+
+ 'Continue? [y/N] ',
|
|
222
|
+
(answer) => {
|
|
223
|
+
rl.close();
|
|
224
|
+
resolve(/^y(es)?$/i.test((answer || '').trim()));
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function ensureConsent({ yes = false } = {}) {
|
|
231
|
+
if (loadConsent()?.opted_in) return true;
|
|
232
|
+
if (yes) {
|
|
233
|
+
saveConsent();
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (!process.stdin.isTTY) {
|
|
237
|
+
throw new Error('Non-interactive terminal. Pass --yes to confirm opt-in submit consent.');
|
|
238
|
+
}
|
|
239
|
+
const ok = await askConsentQuestion();
|
|
240
|
+
if (ok) saveConsent();
|
|
241
|
+
return ok;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Submit unsent feedback entries (opt-in, anonymized).
|
|
246
|
+
*/
|
|
247
|
+
async function submitFeedback(opts = {}) {
|
|
248
|
+
const {
|
|
249
|
+
project,
|
|
250
|
+
all = false,
|
|
251
|
+
persona,
|
|
252
|
+
includePositive = false,
|
|
253
|
+
dryRun = false,
|
|
254
|
+
yes = false,
|
|
255
|
+
limit,
|
|
256
|
+
} = opts;
|
|
257
|
+
|
|
258
|
+
const cfg = resolveSubmitConfig();
|
|
259
|
+
if (!cfg.enabled) {
|
|
260
|
+
throw new Error('Feedback submit is disabled. Set feedback.submit_enabled: true in config.');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let entries = collectUnsentEntries({ project, all, persona, includePositive });
|
|
264
|
+
if (limit != null && Number(limit) > 0) entries = entries.slice(0, Number(limit));
|
|
265
|
+
|
|
266
|
+
if (!entries.length) {
|
|
267
|
+
return { submitted: 0, dryRun, message: 'No unsent feedback entries with comment/correction.' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const payloads = entries.map((e) => entryToSubmitPayload(e, cfg));
|
|
271
|
+
|
|
272
|
+
if (dryRun) {
|
|
273
|
+
return {
|
|
274
|
+
submitted: 0,
|
|
275
|
+
dryRun: true,
|
|
276
|
+
payloads,
|
|
277
|
+
endpoint: cfg.url || '(not configured)',
|
|
278
|
+
message: `Would submit ${payloads.length} anonymized row(s).`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const consented = await ensureConsent({ yes });
|
|
283
|
+
if (!consented) {
|
|
284
|
+
return { submitted: 0, cancelled: true, message: 'Submit cancelled — consent not given.' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const count = await submitRows(payloads, cfg);
|
|
288
|
+
markEntriesSubmitted(entries.map((e) => e.id));
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
submitted: count,
|
|
292
|
+
ids: entries.map((e) => e.id),
|
|
293
|
+
endpoint: cfg.url,
|
|
294
|
+
message: `Submitted ${count} anonymized feedback row(s).`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function submitStatus() {
|
|
299
|
+
const cfg = resolveSubmitConfig();
|
|
300
|
+
const consent = loadConsent();
|
|
301
|
+
const unsent = collectUnsentEntries({ all: true }).length;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
consent: consent?.opted_in ? `opted in (${consent.at})` : 'not opted in',
|
|
305
|
+
endpoint: cfg.url || '(not configured — set feedback.submit_url)',
|
|
306
|
+
anonKey: cfg.anonKey ? 'configured' : '(missing — set feedback.anon_key)',
|
|
307
|
+
installId: cfg.installId,
|
|
308
|
+
unsentCount: unsent,
|
|
309
|
+
packageVersion: cfg.packageVersion,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
anonymizeText,
|
|
315
|
+
entryToSubmitPayload,
|
|
316
|
+
resolveSubmitConfig,
|
|
317
|
+
submitFeedback,
|
|
318
|
+
submitStatus,
|
|
319
|
+
loadConsent,
|
|
320
|
+
saveConsent,
|
|
321
|
+
revokeConsent,
|
|
322
|
+
CONSENT_FILE,
|
|
323
|
+
CONFIG_FILE,
|
|
324
|
+
};
|
package/dist/lib/feedback.js
CHANGED
|
@@ -1 +1,182 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Persona feedback — record when users are unhappy, how they corrected output,
|
|
5
|
+
* and export correction pairs for future training (DPO / LoRA negatives).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const session = require('./session');
|
|
13
|
+
|
|
14
|
+
const { resolvePackageRoot } = require('./platforms');
|
|
15
|
+
const PACKAGE_ROOT = resolvePackageRoot(__dirname);
|
|
16
|
+
const GLOBAL_FEEDBACK_DIR = path.join(os.homedir(), '.analyzthis_design', 'feedback');
|
|
17
|
+
const GLOBAL_FEEDBACK_FILE = path.join(GLOBAL_FEEDBACK_DIR, 'corrections.jsonl');
|
|
18
|
+
|
|
19
|
+
const ISSUE_TAG_HINTS = [
|
|
20
|
+
'wrong_hierarchy',
|
|
21
|
+
'invented_tokens',
|
|
22
|
+
'missed_ds',
|
|
23
|
+
'too_verbose',
|
|
24
|
+
'too_shallow',
|
|
25
|
+
'bad_ia',
|
|
26
|
+
'off_brief',
|
|
27
|
+
'wrong_component',
|
|
28
|
+
'accessibility_miss',
|
|
29
|
+
'business_mismatch',
|
|
30
|
+
'other',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function loadCard(personaId) {
|
|
34
|
+
const cardPath = path.join(PACKAGE_ROOT, 'agents', 'cards', `${personaId}.md`);
|
|
35
|
+
if (!fs.existsSync(cardPath)) return '';
|
|
36
|
+
return fs.readFileSync(cardPath, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseTags(raw) {
|
|
40
|
+
if (!raw) return [];
|
|
41
|
+
return raw.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Record user feedback on a persona's last output.
|
|
46
|
+
* @param {{
|
|
47
|
+
* project?: string,
|
|
48
|
+
* persona: string,
|
|
49
|
+
* rating?: number,
|
|
50
|
+
* comment?: string,
|
|
51
|
+
* correction?: string,
|
|
52
|
+
* tags?: string[],
|
|
53
|
+
* satisfied?: boolean,
|
|
54
|
+
* markRejected?: boolean,
|
|
55
|
+
* }} opts
|
|
56
|
+
*/
|
|
57
|
+
function recordFeedback(opts = {}) {
|
|
58
|
+
const {
|
|
59
|
+
persona,
|
|
60
|
+
rating,
|
|
61
|
+
comment = '',
|
|
62
|
+
correction = '',
|
|
63
|
+
tags = [],
|
|
64
|
+
satisfied = false,
|
|
65
|
+
markRejected = true,
|
|
66
|
+
} = opts;
|
|
67
|
+
|
|
68
|
+
if (!persona) throw new Error('--persona is required');
|
|
69
|
+
|
|
70
|
+
const projectId = opts.project || session.getProjectId();
|
|
71
|
+
const state = session.show({ project: projectId });
|
|
72
|
+
if (!state) throw new Error('No session found. Run: npx analyzthis_design session init');
|
|
73
|
+
|
|
74
|
+
const outputEntry = state.persona_outputs?.[persona];
|
|
75
|
+
if (!outputEntry) {
|
|
76
|
+
throw new Error(`No output recorded for persona "${persona}" in this session yet.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entry = {
|
|
80
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
81
|
+
at: new Date().toISOString(),
|
|
82
|
+
project_id: projectId,
|
|
83
|
+
persona,
|
|
84
|
+
satisfied: !!satisfied,
|
|
85
|
+
rating: rating != null ? Number(rating) : null,
|
|
86
|
+
comment: String(comment).trim(),
|
|
87
|
+
correction: String(correction).trim(),
|
|
88
|
+
tags: tags.length ? tags : (satisfied ? ['positive'] : ['other']),
|
|
89
|
+
original_output: typeof outputEntry.text === 'string'
|
|
90
|
+
? outputEntry.text.slice(0, 8000)
|
|
91
|
+
: JSON.stringify(outputEntry).slice(0, 8000),
|
|
92
|
+
context: {
|
|
93
|
+
task_map_summary: state.digest?.task_map_summary || '',
|
|
94
|
+
problem_type: state.routing_decision?.problem_type || '',
|
|
95
|
+
mode: state.mode || state.digest?.mode || '',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const feedbackLog = Array.isArray(state.feedback_log) ? state.feedback_log.slice() : [];
|
|
100
|
+
feedbackLog.push(entry);
|
|
101
|
+
|
|
102
|
+
if (markRejected && !satisfied) {
|
|
103
|
+
outputEntry.accepted = false;
|
|
104
|
+
outputEntry.feedback_id = entry.id;
|
|
105
|
+
} else if (satisfied) {
|
|
106
|
+
outputEntry.accepted = true;
|
|
107
|
+
outputEntry.feedback_id = entry.id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const persona_outputs = { ...state.persona_outputs, [persona]: outputEntry };
|
|
111
|
+
session.update({
|
|
112
|
+
project: projectId,
|
|
113
|
+
patch: { feedback_log: feedbackLog, persona_outputs },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Append to global cross-project log for aggregate learning
|
|
117
|
+
fs.mkdirSync(GLOBAL_FEEDBACK_DIR, { recursive: true });
|
|
118
|
+
fs.appendFileSync(GLOBAL_FEEDBACK_FILE, JSON.stringify(entry) + '\n');
|
|
119
|
+
|
|
120
|
+
return { entry, projectId, globalFile: GLOBAL_FEEDBACK_FILE };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function listFeedback({ project, all = false } = {}) {
|
|
124
|
+
if (all) {
|
|
125
|
+
const items = [];
|
|
126
|
+
for (const projectId of session.listProjects()) {
|
|
127
|
+
const state = session.show({ project: projectId });
|
|
128
|
+
if (!state?.feedback_log?.length) continue;
|
|
129
|
+
for (const e of state.feedback_log) items.push({ ...e, project_id: projectId });
|
|
130
|
+
}
|
|
131
|
+
if (fs.existsSync(GLOBAL_FEEDBACK_FILE)) {
|
|
132
|
+
// dedupe by id — session copies already included
|
|
133
|
+
}
|
|
134
|
+
return items.sort((a, b) => (a.at < b.at ? 1 : -1));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const projectId = project || session.getProjectId();
|
|
138
|
+
const state = session.show({ project: projectId });
|
|
139
|
+
return state?.feedback_log || [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Export correction pairs for training (rejected output + user correction).
|
|
144
|
+
*/
|
|
145
|
+
function exportCorrections({ persona, project, all = false, output, includePositive = false } = {}) {
|
|
146
|
+
let entries = listFeedback({ project, all });
|
|
147
|
+
if (persona) entries = entries.filter((e) => e.persona === persona);
|
|
148
|
+
if (!includePositive) entries = entries.filter((e) => !e.satisfied);
|
|
149
|
+
|
|
150
|
+
const pairs = [];
|
|
151
|
+
for (const e of entries) {
|
|
152
|
+
if (!e.correction && !e.comment) continue;
|
|
153
|
+
pairs.push({
|
|
154
|
+
persona: e.persona,
|
|
155
|
+
project_id: e.project_id,
|
|
156
|
+
rating: e.rating,
|
|
157
|
+
tags: e.tags,
|
|
158
|
+
system_card: loadCard(e.persona),
|
|
159
|
+
user: e.context?.task_map_summary || '',
|
|
160
|
+
assistant_rejected: e.original_output,
|
|
161
|
+
assistant_preferred: e.correction || e.comment,
|
|
162
|
+
user_comment: e.comment,
|
|
163
|
+
recorded_at: e.at,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const filePath = path.resolve(
|
|
168
|
+
output || path.join(GLOBAL_FEEDBACK_DIR, persona ? `${persona}-corrections.jsonl` : 'all-corrections.jsonl'),
|
|
169
|
+
);
|
|
170
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
171
|
+
fs.writeFileSync(filePath, pairs.map((p) => JSON.stringify(p)).join('\n') + (pairs.length ? '\n' : ''));
|
|
172
|
+
|
|
173
|
+
return { pairs: pairs.length, filePath, entries: entries.length };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
recordFeedback,
|
|
178
|
+
listFeedback,
|
|
179
|
+
exportCorrections,
|
|
180
|
+
ISSUE_TAG_HINTS,
|
|
181
|
+
GLOBAL_FEEDBACK_FILE,
|
|
182
|
+
};
|