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.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/bin/install.cjs +306 -0
- package/bin/promptup-plugin +8 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +123 -0
- package/dist/db.d.ts +35 -0
- package/dist/db.js +327 -0
- package/dist/decision-detector.d.ts +11 -0
- package/dist/decision-detector.js +47 -0
- package/dist/evaluator.d.ts +10 -0
- package/dist/evaluator.js +844 -0
- package/dist/git-activity-extractor.d.ts +35 -0
- package/dist/git-activity-extractor.js +167 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +54 -0
- package/dist/pr-report-generator.d.ts +20 -0
- package/dist/pr-report-generator.js +421 -0
- package/dist/shared/decision-classifier.d.ts +60 -0
- package/dist/shared/decision-classifier.js +385 -0
- package/dist/shared/decision-score.d.ts +7 -0
- package/dist/shared/decision-score.js +31 -0
- package/dist/shared/dimensions.d.ts +43 -0
- package/dist/shared/dimensions.js +361 -0
- package/dist/shared/scoring.d.ts +89 -0
- package/dist/shared/scoring.js +161 -0
- package/dist/shared/types.d.ts +108 -0
- package/dist/shared/types.js +9 -0
- package/dist/tools.d.ts +30 -0
- package/dist/tools.js +456 -0
- package/dist/transcript-parser.d.ts +36 -0
- package/dist/transcript-parser.js +201 -0
- package/hooks/auto-eval.sh +44 -0
- package/hooks/check-update.sh +26 -0
- package/hooks/debug-hook.sh +3 -0
- package/hooks/hooks.json +36 -0
- package/hooks/render-eval.sh +137 -0
- package/package.json +60 -0
- package/skills/eval/SKILL.md +12 -0
- package/skills/pr-report/SKILL.md +37 -0
- package/skills/status/SKILL.md +28 -0
- 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;
|