promptup-plugin 0.1.1

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/bin/install.cjs +306 -0
  4. package/bin/promptup-plugin +8 -0
  5. package/dist/config.d.ts +40 -0
  6. package/dist/config.js +123 -0
  7. package/dist/db.d.ts +35 -0
  8. package/dist/db.js +327 -0
  9. package/dist/decision-detector.d.ts +11 -0
  10. package/dist/decision-detector.js +47 -0
  11. package/dist/evaluator.d.ts +10 -0
  12. package/dist/evaluator.js +844 -0
  13. package/dist/git-activity-extractor.d.ts +35 -0
  14. package/dist/git-activity-extractor.js +167 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +54 -0
  17. package/dist/pr-report-generator.d.ts +20 -0
  18. package/dist/pr-report-generator.js +421 -0
  19. package/dist/shared/decision-classifier.d.ts +60 -0
  20. package/dist/shared/decision-classifier.js +385 -0
  21. package/dist/shared/decision-score.d.ts +7 -0
  22. package/dist/shared/decision-score.js +31 -0
  23. package/dist/shared/dimensions.d.ts +43 -0
  24. package/dist/shared/dimensions.js +361 -0
  25. package/dist/shared/scoring.d.ts +89 -0
  26. package/dist/shared/scoring.js +161 -0
  27. package/dist/shared/types.d.ts +108 -0
  28. package/dist/shared/types.js +9 -0
  29. package/dist/tools.d.ts +30 -0
  30. package/dist/tools.js +456 -0
  31. package/dist/transcript-parser.d.ts +36 -0
  32. package/dist/transcript-parser.js +201 -0
  33. package/hooks/auto-eval.sh +44 -0
  34. package/hooks/check-update.sh +26 -0
  35. package/hooks/debug-hook.sh +3 -0
  36. package/hooks/hooks.json +36 -0
  37. package/hooks/render-eval.sh +137 -0
  38. package/package.json +60 -0
  39. package/skills/eval/SKILL.md +12 -0
  40. package/skills/pr-report/SKILL.md +37 -0
  41. package/skills/status/SKILL.md +28 -0
  42. package/statusline.sh +46 -0
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Heuristic decision classifier for PromptUp.
3
+ *
4
+ * Classifies a user message (in context of the previous assistant turn and
5
+ * tool uses) into one of six decision types using ordered pattern rules.
6
+ * First matching rule wins.
7
+ *
8
+ * STANDALONE copy — no imports from @promptup/shared.
9
+ */
10
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
11
+ function lower(s) {
12
+ return s.toLowerCase();
13
+ }
14
+ function extractFiles(toolUses) {
15
+ if (!toolUses)
16
+ return [];
17
+ const files = [];
18
+ for (const tu of toolUses) {
19
+ if ((tu.name === 'Edit' || tu.name === 'Write') && typeof tu.input.file_path === 'string') {
20
+ files.push(tu.input.file_path);
21
+ }
22
+ }
23
+ return files;
24
+ }
25
+ function hadToolUse(toolUses) {
26
+ return toolUses !== null && toolUses.length > 0;
27
+ }
28
+ /** Extract a short basename from a file path. */
29
+ function basename(filePath) {
30
+ return filePath.split('/').pop() ?? filePath;
31
+ }
32
+ /**
33
+ * Extract a concise description of what the AI did from the previous assistant
34
+ * message and its tool uses (<=80 chars).
35
+ */
36
+ export function extractAiAction(prevAssistantMessage, prevToolUses) {
37
+ const editWriteTools = prevToolUses?.filter(tu => tu.name === 'Edit' || tu.name === 'Write') ?? [];
38
+ const bashGitTools = prevToolUses?.filter(tu => tu.name === 'Bash' && typeof tu.input.command === 'string' && /\bgit\b/.test(tu.input.command)) ?? [];
39
+ if (editWriteTools.length === 1) {
40
+ const tu = editWriteTools[0];
41
+ const filePath = typeof tu.input.file_path === 'string' ? tu.input.file_path : '';
42
+ const verb = tu.name === 'Write' ? 'Created' : 'Edited';
43
+ return `${verb} ${basename(filePath)}`.slice(0, 80);
44
+ }
45
+ if (editWriteTools.length > 1) {
46
+ const names = editWriteTools.map(tu => typeof tu.input.file_path === 'string' ? basename(tu.input.file_path) : '?');
47
+ return `Modified ${editWriteTools.length} files: ${names.join(', ')}`.slice(0, 80);
48
+ }
49
+ if (bashGitTools.length > 0) {
50
+ const cmd = bashGitTools[0].input.command;
51
+ const gitMatch = cmd.match(/git\s+(\w+)/);
52
+ const subCmd = gitMatch?.[1] ?? 'command';
53
+ return `Ran git ${subCmd}`.slice(0, 80);
54
+ }
55
+ // Fallback to first sentence of assistant message
56
+ if (prevAssistantMessage) {
57
+ const stripped = prevAssistantMessage.replace(/<[^>]+>/g, ' ').replace(/\s{2,}/g, ' ').trim();
58
+ const first = stripped.split(/[.!?]/)[0]?.trim() ?? '';
59
+ if (first.length >= 5) {
60
+ return first.slice(0, 80);
61
+ }
62
+ }
63
+ return 'Proposed approach';
64
+ }
65
+ /**
66
+ * Classify signal level: high/medium/low.
67
+ * - high: architectural depth, OR high opinionation, OR reject, OR steer+tactical+
68
+ * - medium: tactical+medium-opinionation, OR validate, OR scope
69
+ * - low: surface depth, OR low-opinionation accept, OR trivial
70
+ */
71
+ export function classifySignal(type, depth, opinionation, devReaction) {
72
+ const trivial = /^(looks?\s+good|lgtm|yes|ok|okay|perfect|great|awesome|nice|approved?|sounds?\s+good)\.?$/i.test(devReaction.trim());
73
+ // Trivial responses are always low
74
+ if (trivial)
75
+ return 'low';
76
+ // Reject is always high (developer overriding AI)
77
+ if (type === 'reject')
78
+ return 'high';
79
+ // Architectural depth -> high
80
+ if (depth === 'architectural')
81
+ return 'high';
82
+ // High opinionation -> high
83
+ if (opinionation === 'high')
84
+ return 'high';
85
+ // Steer with tactical or deeper -> high
86
+ if (type === 'steer' && depth !== 'surface')
87
+ return 'high';
88
+ // Validate, scope -> medium (even if surface/low — these types are inherently meaningful)
89
+ if (type === 'validate' || type === 'scope')
90
+ return 'medium';
91
+ // Surface depth with low opinionation -> low
92
+ if (depth === 'surface' && opinionation === 'low')
93
+ return 'low';
94
+ // Tactical + medium opinionation -> medium
95
+ if (depth === 'tactical' && opinionation === 'medium')
96
+ return 'medium';
97
+ // Accepts with low opinionation -> low
98
+ if (type === 'accept' && opinionation === 'low')
99
+ return 'low';
100
+ // Modify -> medium
101
+ if (type === 'modify')
102
+ return 'medium';
103
+ // Default
104
+ return 'medium';
105
+ }
106
+ /**
107
+ * Build the combined AI->Dev context string (<=120 chars).
108
+ */
109
+ function buildContext(type, aiAction, devReaction) {
110
+ let ctx;
111
+ switch (type) {
112
+ case 'steer':
113
+ ctx = `AI ${aiAction} → Dev redirected: ${devReaction}`;
114
+ break;
115
+ case 'reject':
116
+ ctx = `AI ${aiAction} → Dev rejected: ${devReaction}`;
117
+ break;
118
+ case 'validate':
119
+ ctx = `After ${aiAction} → Dev verified: ${devReaction}`;
120
+ break;
121
+ case 'accept':
122
+ ctx = `AI ${aiAction} → Approved`;
123
+ break;
124
+ case 'modify':
125
+ ctx = `AI ${aiAction} → Dev modified: ${devReaction}`;
126
+ break;
127
+ case 'scope':
128
+ ctx = `Dev ${devReaction}`;
129
+ break;
130
+ default:
131
+ ctx = `AI ${aiAction} → ${devReaction}`;
132
+ }
133
+ if (ctx.length <= 120)
134
+ return ctx;
135
+ return ctx.slice(0, 117) + '...';
136
+ }
137
+ // ─── Rule Patterns ───────────────────────────────────────────────────────────
138
+ // Rule 1: steer — negation + alternative in the same message
139
+ const STEER_NEGATION = /\b(no|don'?t|do not|never|not)\b/i;
140
+ const STEER_ALTERNATIVE = /\b(instead|use\s+\w+|switch\s+to|let'?s\s+(?:use|do|try)|actually\s+(?:let'?s|use|do))\b/i;
141
+ // Rule 2: reject — negation without alternative, only when previous had tool use
142
+ const REJECT_PATTERNS = /\b(won'?t\s+work|that'?s\s+wrong|this\s+is\s+wrong|try\s+again|doesn'?t\s+work|not\s+right|incorrect|wrong|broken|failing|fails|this\s+is\s+broken|doesn'?t\s+make\s+sense)\b/i;
143
+ // Rule 3: modify — explicit change/update requests with a target
144
+ const MODIFY_PATTERNS = /\b(change\s+\S+|update\s+the\s+\w+|add\s+(?:error\s+handling|logging|validation|retry)\s+to)\b/i;
145
+ // Rule 4: validate — verification / test requests
146
+ const VALIDATE_PATTERNS = /\b(run\s+the\s+tests?|check\s+if|does\s+this\s+handle|what\s+happens\s+when|verify|make\s+sure\s+(?:it|this)|assert|confirm\s+that)\b/i;
147
+ // Rule 5: scope remove
148
+ const SCOPE_REMOVE_PATTERNS = /\b(skip|drop|don'?t\s+need|leave\s+out|remove|cut|omit|for\s+now\s+skip|skip\s+(?:the\s+)?\w+\s+for\s+now)\b/i;
149
+ // Rule 6: scope add
150
+ const SCOPE_ADD_PATTERNS = /\b(also\s+add|additionally\s+include|additionally\s+add|we\s+still\s+need|also\s+include|also\s+(?:make|create|implement|set\s+up))\b/i;
151
+ // Rule 7: accept explicit affirmatives
152
+ const ACCEPT_EXPLICIT_PATTERNS = /\b(looks?\s+good|lgtm|yes|do\s+it|perfect|great|awesome|nice|ship\s+it|proceed|go\s+ahead|sounds?\s+good|ok(?:ay)?|approved?|that'?s\s+(?:good|right|correct|great|perfect))\b/i;
153
+ // ─── Depth Heuristics ─────────────────────────────────────────────────────────
154
+ const SURFACE_SIMPLE_WORDS = /^(yes|no|ok|okay|sure|fine|yep|nope|lgtm|great|perfect|awesome|nice|cool|thanks|good)\.?$/i;
155
+ const SURFACE_NAMING_FORMATTING = /\b(rename|naming|camelCase|snake_case|formatting|indent|whitespace|typo|spelling|capitalize)\b/i;
156
+ const TACTICAL_KEYWORDS = /\b(bcrypt|argon2|redis|postgres|sqlite|jwt|oauth|middleware|express|fastify|prisma|drizzle|zod|vitest|jest|playwright|webpack|vite|eslint|prettier|docker|nginx|cors|rate.?limit|cache|queue|worker|hook|endpoint|route|handler|controller|service|repository|schema|migration|index|foreign.?key|transaction)\b/i;
157
+ const ARCHITECTURAL_KEYWORDS = /\b(system\s+design|data\s+flow|scaling|infrastructure|split\s+(?:into|the)\s+service|event.?driven|microservice|monorepo|database\s+schema|redesign|architecture|refactor\s+(?:the|all|entire)|separate\s+concern|decoupl|abstraction|domain\s+model|bounded\s+context|api\s+contract|versioning|deployment|ci\/cd|pipeline|load\s+balanc|sharding|replication)\b/i;
158
+ function countTechnicalConcepts(msg) {
159
+ const m = lower(msg);
160
+ const concepts = [
161
+ /\b(bcrypt|argon2|redis|postgres|sqlite|jwt|oauth|cors|rate.?limit)\b/,
162
+ /\b(middleware|express|fastify|hook|endpoint|route|handler|controller)\b/,
163
+ /\b(service|repository|schema|migration|index|foreign.?key|transaction)\b/,
164
+ /\b(cache|queue|worker|async|await|promise|stream|buffer)\b/,
165
+ /\b(docker|nginx|kubernetes|ci\/cd|pipeline|deploy)\b/,
166
+ /\b(prisma|drizzle|zod|vitest|jest|playwright|webpack|vite|eslint)\b/,
167
+ ];
168
+ return concepts.filter(re => re.test(m)).length;
169
+ }
170
+ export function classifyDepth(msg) {
171
+ const trimmed = msg.trim();
172
+ const msgLower = lower(trimmed);
173
+ // surface: very short, simple yes/no/ok, or only naming/formatting
174
+ if (trimmed.length < 30)
175
+ return 'surface';
176
+ if (SURFACE_SIMPLE_WORDS.test(trimmed))
177
+ return 'surface';
178
+ if (SURFACE_NAMING_FORMATTING.test(msgLower) && trimmed.length < 80)
179
+ return 'surface';
180
+ // architectural: system design keywords OR 3+ technical concepts
181
+ if (ARCHITECTURAL_KEYWORDS.test(msgLower))
182
+ return 'architectural';
183
+ if (countTechnicalConcepts(trimmed) >= 3)
184
+ return 'architectural';
185
+ // tactical: specific tools, libraries, patterns
186
+ if (TACTICAL_KEYWORDS.test(msgLower))
187
+ return 'tactical';
188
+ // default: surface for short messages, tactical for longer ones
189
+ if (trimmed.length < 60)
190
+ return 'surface';
191
+ return 'tactical';
192
+ }
193
+ // ─── Opinionation Heuristics ──────────────────────────────────────────────────
194
+ const LOW_OPINIONATION = /^(looks?\s+good|lgtm|yes|do\s+it|perfect|great|awesome|nice|ship\s+it|proceed|go\s+ahead|sounds?\s+good|ok(?:ay)?|approved?|run\s+the\s+tests?|that'?s\s+(?:good|right|correct|great|perfect))\.?$/i;
195
+ const HIGH_OPINIONATION_KEYWORDS = /\b(compliance|regulation|gdpr|hipaa|soc2|business\s+requirement|customer|user\s+behavior|user\s+experience|pricing|deadline|team\s+decision|stakeholder|legal|audit|security\s+policy|performance\s+budget|sla|slo|kpi|okr|revenue|churn|conversion)\b/i;
196
+ function hasProjectSpecificReferences(msg) {
197
+ // References to specific files, 'we use X', 'in this project', 'our X'
198
+ if (/\b(we\s+use|in\s+this\s+project|our\s+(codebase|api|backend|frontend|db|database|schema|service)|this\s+(repo|codebase|project))\b/i.test(msg))
199
+ return true;
200
+ if (/\.[a-z]{1,5}['"]?\s*(?:file|path|module)?/i.test(msg))
201
+ return true;
202
+ // References to specific named libraries/tools in a choice context
203
+ if (TACTICAL_KEYWORDS.test(lower(msg)))
204
+ return true;
205
+ return false;
206
+ }
207
+ function countSentences(msg) {
208
+ return msg.split(/[.!?]+/).filter(s => s.trim().length > 3).length;
209
+ }
210
+ export function classifyOpinionation(msg) {
211
+ const trimmed = msg.trim();
212
+ // low: generic short responses with no domain knowledge
213
+ if (LOW_OPINIONATION.test(trimmed))
214
+ return 'low';
215
+ if (trimmed.length < 20)
216
+ return 'low';
217
+ // high: introduces external/business knowledge OR multi-sentence reasoning explaining WHY
218
+ if (HIGH_OPINIONATION_KEYWORDS.test(trimmed))
219
+ return 'high';
220
+ if (countSentences(trimmed) >= 3 && trimmed.length > 100)
221
+ return 'high';
222
+ // medium: references project-specific things
223
+ if (hasProjectSpecificReferences(trimmed))
224
+ return 'medium';
225
+ // default based on length / specificity
226
+ if (trimmed.length > 80)
227
+ return 'medium';
228
+ return 'low';
229
+ }
230
+ // ─── Context Rewriting ────────────────────────────────────────────────────────
231
+ /** Strip XML/system tags from a message. */
232
+ function stripSystemContent(msg) {
233
+ return msg
234
+ .replace(/<[^>]+>/g, ' ') // remove XML tags
235
+ .replace(/\s{2,}/g, ' ')
236
+ .trim();
237
+ }
238
+ /**
239
+ * Convert a raw user message into a clean decision summary (<=80 chars).
240
+ * Not a quote — a reworded description of what the decision was.
241
+ */
242
+ export function summarizeContext(userMessage, type) {
243
+ const msg = stripSystemContent(userMessage).trim();
244
+ const msgLower = lower(msg);
245
+ // Accept / affirmative patterns (check before length-based fallbacks)
246
+ if (/^(looks?\s+good|lgtm|approved?)[.!]?$/i.test(msg)) {
247
+ return 'Approved implementation';
248
+ }
249
+ if (/^(great|perfect|awesome|nice|ship\s+it|sounds?\s+good|yes|ok|okay)[.!]?$/i.test(msg)) {
250
+ return 'Approved implementation';
251
+ }
252
+ if (/\b(looks?\s+good|lgtm|approved?)\b/i.test(msgLower) && msg.length < 30) {
253
+ return 'Approved implementation';
254
+ }
255
+ if (/\b(great|perfect|awesome|nice|ship\s+it|sounds?\s+good)\b/i.test(msgLower) && msg.length < 30) {
256
+ return 'Approved implementation';
257
+ }
258
+ // Very short messages
259
+ if (msg.length <= 10) {
260
+ if (/^(yes|ok|okay|sure|yep)\.?$/i.test(msg))
261
+ return 'Approved implementation';
262
+ if (/^(no|nope)\.?$/i.test(msg))
263
+ return 'Rejected approach';
264
+ return msg.slice(0, 80);
265
+ }
266
+ // Validate patterns
267
+ if (/\brun\s+the\s+tests?\b/i.test(msgLower))
268
+ return 'Requested test verification';
269
+ if (/\brun\s+(?:the\s+)?(\w+)\s+tests?\b/i.test(msgLower)) {
270
+ const match = msg.match(/\brun\s+(?:the\s+)?(\w+)\s+tests?\b/i);
271
+ return `Requested ${match?.[1] ?? ''} test verification`.trim();
272
+ }
273
+ if (/\b(verify|confirm\s+that|make\s+sure|check\s+if)\b/i.test(msgLower)) {
274
+ return 'Requested verification';
275
+ }
276
+ // Reject patterns
277
+ if (/\b(won'?t\s+work|doesn'?t\s+work|this\s+is\s+wrong|not\s+right|incorrect|broken)\b/i.test(msgLower)) {
278
+ return 'Rejected approach';
279
+ }
280
+ // Steer: "No, use X instead of Y"
281
+ const steerMatch = msg.match(/(?:no[,.]?\s+)?(?:use|switch\s+to|let'?s\s+use)\s+(\w+)\s+instead\s+of\s+(\w+)/i);
282
+ if (steerMatch) {
283
+ return `Chose ${steerMatch[1]} over ${steerMatch[2]}`.slice(0, 80);
284
+ }
285
+ // Steer: "No, use X" without "instead of"
286
+ const steerUseMatch = msg.match(/(?:no[,.]?\s+)?(?:use|switch\s+to)\s+([\w-]+)(?:\s+instead)?/i);
287
+ if (steerUseMatch && type === 'steer') {
288
+ return `Steered to use ${steerUseMatch[1]}`.slice(0, 80);
289
+ }
290
+ // Scope add
291
+ if (/\b(also\s+add|additionally\s+(?:add|include)|we\s+still\s+need|also\s+include|also\s+(?:make|create|implement))\b/i.test(msgLower)) {
292
+ // Extract what's being added
293
+ const scopeMatch = msg.match(/(?:also\s+add|also\s+include|also\s+(?:make|create|implement)|we\s+still\s+need|additionally\s+(?:add|include))\s+(.+)/i);
294
+ if (scopeMatch) {
295
+ return `Added ${scopeMatch[1].slice(0, 60)} to scope`.slice(0, 80);
296
+ }
297
+ return 'Expanded task scope';
298
+ }
299
+ // Scope remove
300
+ if (/\b(skip|drop|don'?t\s+need|leave\s+out|omit)\b/i.test(msgLower)) {
301
+ return 'Removed item from scope';
302
+ }
303
+ // Modify
304
+ if (/\bchange\s+(\S+)/i.test(msgLower)) {
305
+ const m = msg.match(/\bchange\s+(\S+)/i);
306
+ return `Changed ${m?.[1] ?? 'implementation'}`.slice(0, 80);
307
+ }
308
+ if (/\bupdate\s+the\s+(\w+)/i.test(msgLower)) {
309
+ const m = msg.match(/\bupdate\s+the\s+(\w+)/i);
310
+ return `Updated ${m?.[1] ?? 'implementation'}`.slice(0, 80);
311
+ }
312
+ // For longer messages: take the first meaningful sentence and trim
313
+ const firstSentence = msg.split(/[.!?]/)[0]?.trim() ?? msg;
314
+ if (firstSentence.length <= 80)
315
+ return firstSentence;
316
+ return firstSentence.slice(0, 77) + '...';
317
+ }
318
+ // ─── Classifier ──────────────────────────────────────────────────────────────
319
+ /**
320
+ * Classify a user message into a decision type using heuristic rules.
321
+ *
322
+ * @param userMessage The incoming user message to classify.
323
+ * @param prevAssistantMessage The preceding assistant message (or null).
324
+ * @param prevToolUses Tool uses from the preceding assistant turn (or null).
325
+ * @returns ClassifiedDecision if a rule matched, otherwise null.
326
+ */
327
+ export function classifyDecision(userMessage, prevAssistantMessage, prevToolUses) {
328
+ const msg = userMessage.trim();
329
+ const msgLower = lower(msg);
330
+ const filesAffected = extractFiles(prevToolUses);
331
+ const hadCode = hadToolUse(prevToolUses);
332
+ const depth = classifyDepth(msg);
333
+ const opinionation = classifyOpinionation(msg);
334
+ const aiAction = extractAiAction(prevAssistantMessage, prevToolUses);
335
+ function result(type, matchedRule) {
336
+ const devReaction = summarizeContext(msg, type);
337
+ const signal = classifySignal(type, depth, opinionation, msg);
338
+ return {
339
+ type,
340
+ context: buildContext(type, aiAction, devReaction),
341
+ aiAction,
342
+ devReaction,
343
+ matchedRule,
344
+ filesAffected,
345
+ depth,
346
+ opinionation,
347
+ signal,
348
+ };
349
+ }
350
+ // Rule 1: steer — negation + alternative (highest priority)
351
+ if (STEER_NEGATION.test(msgLower) && STEER_ALTERNATIVE.test(msgLower)) {
352
+ return result('steer', 'steer_negation_plus_alternative');
353
+ }
354
+ // Rule 2: reject — negation without alternative ONLY when previous had tool_use
355
+ if (hadCode && REJECT_PATTERNS.test(msgLower)) {
356
+ return result('reject', 'reject_negation_after_code');
357
+ }
358
+ // Rule 3: modify — explicit change request
359
+ if (MODIFY_PATTERNS.test(msgLower)) {
360
+ return result('modify', 'modify_change_request');
361
+ }
362
+ // Rule 4: validate — verification/test request
363
+ if (VALIDATE_PATTERNS.test(msgLower)) {
364
+ return result('validate', 'validate_verification_request');
365
+ }
366
+ // Rule 5: scope remove
367
+ if (SCOPE_REMOVE_PATTERNS.test(msgLower)) {
368
+ return result('scope', 'scope_remove');
369
+ }
370
+ // Rule 6: scope add
371
+ if (SCOPE_ADD_PATTERNS.test(msgLower)) {
372
+ return result('scope', 'scope_add');
373
+ }
374
+ // Rule 7: accept explicit affirmative
375
+ if (ACCEPT_EXPLICIT_PATTERNS.test(msgLower)) {
376
+ return result('accept', 'accept_explicit_affirmative');
377
+ }
378
+ // Rule 8: accept implicit — any message >10 chars after code generation
379
+ // that didn't match any rule above
380
+ if (hadCode && msg.length > 10) {
381
+ return result('accept', 'accept_implicit_new_request');
382
+ }
383
+ // No rule matched — ambiguous
384
+ return null;
385
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Decision Quality Score (DQS) computation for PromptUp.
3
+ *
4
+ * STANDALONE copy — no imports from @promptup/shared.
5
+ */
6
+ import type { DecisionRow } from './types.js';
7
+ export declare function computeDQS(decisions: DecisionRow[], validationRate: number): number | null;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Decision Quality Score (DQS) computation for PromptUp.
3
+ *
4
+ * STANDALONE copy — no imports from @promptup/shared.
5
+ */
6
+ function countByType(decisions) {
7
+ const counts = { steer: 0, accept: 0, reject: 0, modify: 0, validate: 0, scope: 0 };
8
+ for (const d of decisions)
9
+ counts[d.type] = (counts[d.type] || 0) + 1;
10
+ return counts;
11
+ }
12
+ function clamp(min, max, value) {
13
+ return Math.max(min, Math.min(max, value));
14
+ }
15
+ export function computeDQS(decisions, validationRate) {
16
+ const total = decisions.length;
17
+ if (total === 0)
18
+ return null;
19
+ const b = countByType(decisions);
20
+ // Autonomy: steering + rejecting + modifying vs total
21
+ const autonomy = (b.steer + b.reject + b.modify) / total;
22
+ // Discipline: modifications vs all acceptances
23
+ const acceptsAndModifies = b.accept + b.modify;
24
+ const discipline = acceptsAndModifies > 0 ? b.modify / acceptsAndModifies : 0.5;
25
+ // Validation rate (0-1)
26
+ const validation = clamp(0, 1, validationRate);
27
+ // Diversity: types used (peaks at 4+)
28
+ const typesUsed = Object.values(b).filter(v => v > 0).length;
29
+ const diversity = Math.min(typesUsed / 4, 1);
30
+ return clamp(0, 100, Math.round(autonomy * 25 + discipline * 25 + validation * 30 + diversity * 20));
31
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Base 6-dimension rubric definitions for PromptUp evaluation.
3
+ * Sourced from the PromptUp Evaluator rubric v0.1.
4
+ *
5
+ * STANDALONE copy — no imports from @promptup/shared.
6
+ */
7
+ import type { WeightProfileKey } from './types.js';
8
+ export interface DimensionDefinition {
9
+ key: string;
10
+ label: string;
11
+ description: string;
12
+ scoring_guidance: string;
13
+ signals: string[];
14
+ ranges: {
15
+ min: number;
16
+ max: number;
17
+ description: string;
18
+ }[];
19
+ }
20
+ export declare const BASE_DIMENSION_KEYS: readonly ["task_decomposition", "prompt_specificity", "output_validation", "iteration_quality", "strategic_tool_usage", "context_management"];
21
+ export type BaseDimensionKey = (typeof BASE_DIMENSION_KEYS)[number];
22
+ export declare const BASE_DIMENSIONS: Record<BaseDimensionKey, DimensionDefinition>;
23
+ export declare const DOMAIN_DIMENSION_KEYS: readonly ["architectural_awareness", "error_anticipation", "technical_vocabulary", "dependency_reasoning", "tradeoff_articulation"];
24
+ export type DomainDimensionKey = (typeof DOMAIN_DIMENSION_KEYS)[number];
25
+ export declare const DOMAIN_DIMENSIONS: Record<DomainDimensionKey, DimensionDefinition>;
26
+ /** All 11 dimension keys (6 base + 5 domain) */
27
+ export declare const ALL_DIMENSION_KEYS: readonly ["task_decomposition", "prompt_specificity", "output_validation", "iteration_quality", "strategic_tool_usage", "context_management", "architectural_awareness", "error_anticipation", "technical_vocabulary", "dependency_reasoning", "tradeoff_articulation"];
28
+ export type AllDimensionKey = (typeof ALL_DIMENSION_KEYS)[number];
29
+ /** Default base dimension configuration for a new rubric */
30
+ export declare const DEFAULT_BASE_DIMENSIONS: Record<BaseDimensionKey, {
31
+ weight: number;
32
+ enabled: boolean;
33
+ }>;
34
+ export declare const WEIGHT_PROFILE_KEYS: readonly ["balanced", "greenfield", "bugfix", "refactor", "security_review"];
35
+ export interface WeightProfile {
36
+ key: WeightProfileKey;
37
+ label: string;
38
+ description: string;
39
+ weights: Record<BaseDimensionKey, number>;
40
+ }
41
+ export declare const WEIGHT_PROFILES: Record<WeightProfileKey, WeightProfile>;
42
+ /** Look up a weight profile by key. Returns undefined if not found. */
43
+ export declare function getWeightProfile(key: WeightProfileKey): WeightProfile | undefined;