replay-labs 0.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/LICENSE +22 -0
- package/README.md +134 -0
- package/examples/password-reset-transcript.md +27 -0
- package/examples/password-reset.diff +101 -0
- package/package.json +47 -0
- package/scripts/capture-git-working-diff.js +56 -0
- package/scripts/create-added-files-diff.js +33 -0
- package/scripts/extract-claude-transcript.js +86 -0
- package/scripts/extract-codex-transcript.js +119 -0
- package/src/cli.js +316 -0
- package/src/discovery.js +715 -0
- package/src/generate.js +406 -0
- package/src/ingest.js +124 -0
- package/src/interaction.js +1161 -0
- package/src/lab-ui.js +1339 -0
- package/src/modules.js +643 -0
- package/src/overview.js +147 -0
- package/src/patterns.js +322 -0
- package/src/pipeline.js +68 -0
- package/src/report.js +516 -0
- package/src/review.js +238 -0
- package/src/server.js +199 -0
- package/src/storage.js +34 -0
package/src/report.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
const MAX_SNIPPET_LINES = 8;
|
|
2
|
+
|
|
3
|
+
const DECISION_RULES = [
|
|
4
|
+
{
|
|
5
|
+
id: "next-client-boundary",
|
|
6
|
+
title: "Put browser-only voice behavior behind a client component boundary",
|
|
7
|
+
patterns: [/'use client'|"use client"/i, /SpeechRecognition/i, /speechSynthesis/i, /localStorage/i],
|
|
8
|
+
why:
|
|
9
|
+
"Browser speech APIs and localStorage only exist in the browser. In a Next.js app, that pushes the interactive voice experience into a client component.",
|
|
10
|
+
beginnerMiss:
|
|
11
|
+
"A beginner may call browser APIs from server-rendered code and only discover the problem at runtime or hydration time.",
|
|
12
|
+
seniorCheck:
|
|
13
|
+
"A senior engineer would check that browser APIs are guarded, loading states are explicit, and unsupported browsers fail gracefully.",
|
|
14
|
+
alternatives: [
|
|
15
|
+
"Browser-first demo: use Web Speech API and Speech Synthesis in a client component.",
|
|
16
|
+
"Server-driven voice: use a phone provider or realtime audio API for more consistent voice behavior.",
|
|
17
|
+
"Fragile: assume every browser supports the same speech APIs."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "api-secret-boundary",
|
|
22
|
+
title: "Keep model and telephony credentials behind API routes",
|
|
23
|
+
patterns: [/app\/api/i, /ANTHROPIC_API_KEY/i, /Anthropic/i, /twilio/i, /NextRequest/i, /NextResponse/i],
|
|
24
|
+
why:
|
|
25
|
+
"API routes let the browser call application behavior without exposing provider secrets. This is the boundary between demo UI and trusted server work.",
|
|
26
|
+
beginnerMiss:
|
|
27
|
+
"A beginner may put provider calls directly in the browser because it feels simpler, accidentally leaking credentials or making the app impossible to deploy safely.",
|
|
28
|
+
seniorCheck:
|
|
29
|
+
"A senior engineer would inspect environment variables, error handling, request validation, and whether provider failures produce usable user feedback.",
|
|
30
|
+
alternatives: [
|
|
31
|
+
"Simple demo: call a Next.js API route that wraps the provider SDK.",
|
|
32
|
+
"Production-ready: add validation, rate limits, structured errors, and observability.",
|
|
33
|
+
"Unsafe: call Anthropic or Twilio directly from client code."
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "demo-persistence",
|
|
38
|
+
title: "Use simple persistence only when the demo constraint is explicit",
|
|
39
|
+
patterns: [/goals\.json/i, /fs\.writeFileSync/i, /fs\.readFileSync/i, /in-memory store/i, /new Map/i],
|
|
40
|
+
why:
|
|
41
|
+
"A text or JSON file can be the right choice for a single-user demo because it keeps the concept visible and avoids database setup.",
|
|
42
|
+
beginnerMiss:
|
|
43
|
+
"A beginner may confuse demo persistence with production persistence and miss concurrency, deployment, data loss, and multi-user problems.",
|
|
44
|
+
seniorCheck:
|
|
45
|
+
"A senior engineer would verify that the file-store limitation is documented and that the next production step is obvious.",
|
|
46
|
+
alternatives: [
|
|
47
|
+
"Demo: store goals in a local JSON file.",
|
|
48
|
+
"Small production app: use SQLite, Postgres, or a hosted datastore.",
|
|
49
|
+
"Wrong abstraction: add a complex data layer before proving the product loop."
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "prompt-protocol",
|
|
54
|
+
title: "Define a small protocol between the model and app logic",
|
|
55
|
+
patterns: [/SAVE_GOALS/i, /parseGoals/i, /MORNING_SYSTEM/i, /EVENING_SYSTEM/i, /system/i],
|
|
56
|
+
why:
|
|
57
|
+
"When an LLM response drives application state, the app needs a predictable protocol for extracting structured intent from natural language.",
|
|
58
|
+
beginnerMiss:
|
|
59
|
+
"A beginner may parse free-form prose casually and end up with brittle behavior whenever the model phrases things differently.",
|
|
60
|
+
seniorCheck:
|
|
61
|
+
"A senior engineer would check parsing robustness, prompt constraints, tests around malformed output, and whether structured tool calls would be better.",
|
|
62
|
+
alternatives: [
|
|
63
|
+
"Prototype: use a sentinel like SAVE_GOALS with JSON payload.",
|
|
64
|
+
"Better: use structured outputs or tool calls where available.",
|
|
65
|
+
"Fragile: infer goals from arbitrary prose without a contract."
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "product-loop-first",
|
|
70
|
+
title: "Validate the product loop before hardening the platform",
|
|
71
|
+
patterns: [/morning/i, /evening/i, /accountability/i, /check-in/i, /goals/i],
|
|
72
|
+
why:
|
|
73
|
+
"The core product question is whether a morning commitment and evening check-in feels useful. Implementation choices should serve that loop first.",
|
|
74
|
+
beginnerMiss:
|
|
75
|
+
"A beginner may optimize infrastructure, styling, or integrations before proving that the human workflow is compelling.",
|
|
76
|
+
seniorCheck:
|
|
77
|
+
"A senior engineer would ask whether each technical choice helps validate the behavioral loop or distracts from it.",
|
|
78
|
+
alternatives: [
|
|
79
|
+
"Loop-first: build the smallest morning/evening check-in that can be tried today.",
|
|
80
|
+
"Integration-first: connect Notion, calendars, SMS, and billing before validating the habit loop.",
|
|
81
|
+
"Research-first: prototype the conversation script manually before coding."
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "api-boundary-validation",
|
|
86
|
+
title: "Validate at the API boundary",
|
|
87
|
+
patterns: [/validate/i, /schema/i, /zod/i, /required/i],
|
|
88
|
+
why:
|
|
89
|
+
"Boundary validation keeps invalid or incomplete input out of the business logic. It also makes failure behavior easier to test and reason about.",
|
|
90
|
+
beginnerMiss:
|
|
91
|
+
"A beginner may only check that the happy path works and miss that malformed input should fail before any state changes happen.",
|
|
92
|
+
seniorCheck:
|
|
93
|
+
"A senior engineer would verify that validation errors are explicit, test-covered, and consistent with the rest of the API.",
|
|
94
|
+
alternatives: [
|
|
95
|
+
"Prototype: check only the fields needed for the immediate happy path.",
|
|
96
|
+
"Production-ready: use a shared schema or validation helper so routes behave consistently.",
|
|
97
|
+
"Fragile: trust the client to send valid data."
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "token-lifecycle",
|
|
102
|
+
title: "Treat tokens as lifecycle-bound state",
|
|
103
|
+
patterns: [/token/i, /expires/i, /expiry/i, /reset/i],
|
|
104
|
+
why:
|
|
105
|
+
"Password reset, invitation, and verification tokens are temporary authority. Their creation, expiry, and consumption rules define the security boundary.",
|
|
106
|
+
beginnerMiss:
|
|
107
|
+
"A beginner may store or check a token without thinking through reuse, expiration, or whether failed attempts leak account information.",
|
|
108
|
+
seniorCheck:
|
|
109
|
+
"A senior engineer would check expiration, one-time use, hashing at rest, account enumeration behavior, and tests for invalid tokens.",
|
|
110
|
+
alternatives: [
|
|
111
|
+
"Simple: store a reset token and expiry directly on the user record.",
|
|
112
|
+
"More robust: store hashed tokens in a separate table with explicit consumed/expired state.",
|
|
113
|
+
"Risky: persist raw tokens indefinitely."
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "test-regression-net",
|
|
118
|
+
title: "Use tests as the regression net",
|
|
119
|
+
patterns: [/test/i, /expect/i, /describe/i, /it\(/i, /assert/i],
|
|
120
|
+
why:
|
|
121
|
+
"Tests turn the session's intended behavior into an executable contract, which matters when AI-generated changes are revised later.",
|
|
122
|
+
beginnerMiss:
|
|
123
|
+
"A beginner may treat tests as proof that the code is correct without checking whether the tests cover important edge cases.",
|
|
124
|
+
seniorCheck:
|
|
125
|
+
"A senior engineer would look for tests that fail for the right reason before the fix and cover both happy path and important failure modes.",
|
|
126
|
+
alternatives: [
|
|
127
|
+
"Fast prototype: add one high-signal integration test.",
|
|
128
|
+
"Production-ready: cover happy path, invalid input, expired state, and repeated actions.",
|
|
129
|
+
"Weak: snapshot only the final UI or output without asserting behavior."
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "cli-contract",
|
|
134
|
+
title: "Design the CLI as a stable product contract",
|
|
135
|
+
patterns: [/bin":/i, /parseArgs/i, /Usage:/i, /process\.argv/i, /--goal|--diff|--transcript|--out/i],
|
|
136
|
+
why:
|
|
137
|
+
"A CLI command is a user-facing contract. Its flags, errors, usage text, and output paths define how people will automate and trust the tool.",
|
|
138
|
+
beginnerMiss:
|
|
139
|
+
"A beginner may focus on making the script run once and miss the importance of predictable flags, helpful errors, and repeatable output.",
|
|
140
|
+
seniorCheck:
|
|
141
|
+
"A senior engineer would verify required arguments, failure messages, exit codes, path handling, and whether the command shape matches the product's core workflow.",
|
|
142
|
+
alternatives: [
|
|
143
|
+
"Prototype: accept explicit file paths and write a markdown report.",
|
|
144
|
+
"More robust: support stdin, glob inputs, config files, and structured JSON output.",
|
|
145
|
+
"Fragile: rely on positional arguments with unclear ordering."
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "schema-first-reporting",
|
|
150
|
+
title: "Keep the report schema stricter than the model output",
|
|
151
|
+
patterns: [/Decision Cards/i, /Understanding Checklist/i, /Quiz/i, /generateLearningReport/i, /formatDecisions/i],
|
|
152
|
+
why:
|
|
153
|
+
"A strict report schema protects the product from becoming a generic summary generator. It forces every output to teach decisions, risks, alternatives, and mastery checks.",
|
|
154
|
+
beginnerMiss:
|
|
155
|
+
"A beginner may ask an LLM to summarize the session and accept fluent prose even when it does not teach judgment.",
|
|
156
|
+
seniorCheck:
|
|
157
|
+
"A senior engineer would check whether the schema creates consistent, reviewable sections and whether each section is grounded in session evidence.",
|
|
158
|
+
alternatives: [
|
|
159
|
+
"Loose: ask for a narrative summary of what happened.",
|
|
160
|
+
"Schema-first: require decisions, risks, alternatives, checklist, quiz, and evidence.",
|
|
161
|
+
"Evaluator-driven: score reports against a rubric for decision specificity and usefulness."
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "fixture-driven-product-learning",
|
|
166
|
+
title: "Use fixtures to make product quality inspectable",
|
|
167
|
+
patterns: [/examples\//i, /reports\//i, /password-reset/i, /fixture/i],
|
|
168
|
+
why:
|
|
169
|
+
"Fixtures make the product idea concrete. They let the team inspect whether the output feels educational before investing in heavier UI or integrations.",
|
|
170
|
+
beginnerMiss:
|
|
171
|
+
"A beginner may skip examples and only test implementation mechanics, making it harder to judge the user experience.",
|
|
172
|
+
seniorCheck:
|
|
173
|
+
"A senior engineer would keep representative fixtures and compare report quality across different session types.",
|
|
174
|
+
alternatives: [
|
|
175
|
+
"Fast: maintain one hand-written fixture that exercises the report shape.",
|
|
176
|
+
"Better: add several real anonymized sessions across frontend, backend, debugging, and refactoring work.",
|
|
177
|
+
"Weak: test only that files are written without checking report usefulness."
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "error-handling",
|
|
182
|
+
title: "Make failure behavior explicit",
|
|
183
|
+
patterns: [/throw/i, /catch/i, /error/i, /status/i, /400|401|403|404|500/],
|
|
184
|
+
why:
|
|
185
|
+
"Explicit failure handling makes the system predictable for users, callers, and future maintainers.",
|
|
186
|
+
beginnerMiss:
|
|
187
|
+
"A beginner may let exceptions bubble accidentally or return vague errors that make debugging and UX worse.",
|
|
188
|
+
seniorCheck:
|
|
189
|
+
"A senior engineer would check that expected failures are handled intentionally and unexpected failures are still observable.",
|
|
190
|
+
alternatives: [
|
|
191
|
+
"Simple: return clear status codes and messages at the route layer.",
|
|
192
|
+
"Production-ready: use typed errors or a shared error mapper.",
|
|
193
|
+
"Fragile: catch every error and return a generic success-like response."
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "dependency-choice",
|
|
198
|
+
title: "Add dependencies only when they buy enough clarity",
|
|
199
|
+
patterns: [/package\.json/i, /^\+\s*"dependencies":/m, /^\+\s*"devDependencies":/m],
|
|
200
|
+
why:
|
|
201
|
+
"A library can reduce implementation risk, but it also adds API surface, update burden, and conventions the team must understand.",
|
|
202
|
+
beginnerMiss:
|
|
203
|
+
"A beginner may add a library because it solves the immediate task without weighing whether the project already has a simpler pattern.",
|
|
204
|
+
seniorCheck:
|
|
205
|
+
"A senior engineer would check bundle/runtime impact, maintenance quality, security posture, and fit with existing project conventions.",
|
|
206
|
+
alternatives: [
|
|
207
|
+
"No dependency: implement the small behavior directly if it is truly simple.",
|
|
208
|
+
"Use existing dependency: prefer a tool already present in the project.",
|
|
209
|
+
"New dependency: add one when it removes meaningful complexity or risk."
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
export function generateLearningReport({ goal, diff, transcript, diffPath, transcriptPath }) {
|
|
215
|
+
const analysis = analyzeSession({ goal, diff, transcript, diffPath, transcriptPath });
|
|
216
|
+
|
|
217
|
+
return formatLearningReport(analysis);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function analyzeSession({ goal, diff, transcript, diffPath, transcriptPath }) {
|
|
221
|
+
const changedFiles = extractChangedFiles(diff);
|
|
222
|
+
const stats = summarizeDiff(diff);
|
|
223
|
+
const transcriptSignals = extractTranscriptSignals(transcript);
|
|
224
|
+
const decisions = extractDecisions({ diff, transcript });
|
|
225
|
+
const risks = extractRisks({ diff, transcript });
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
goal,
|
|
229
|
+
diff,
|
|
230
|
+
transcript,
|
|
231
|
+
diffPath,
|
|
232
|
+
transcriptPath,
|
|
233
|
+
changedFiles,
|
|
234
|
+
stats,
|
|
235
|
+
transcriptSignals,
|
|
236
|
+
decisions,
|
|
237
|
+
risks
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatLearningReport(analysis) {
|
|
242
|
+
const {
|
|
243
|
+
goal,
|
|
244
|
+
diff,
|
|
245
|
+
transcript,
|
|
246
|
+
diffPath,
|
|
247
|
+
transcriptPath,
|
|
248
|
+
changedFiles,
|
|
249
|
+
stats,
|
|
250
|
+
transcriptSignals,
|
|
251
|
+
decisions,
|
|
252
|
+
risks
|
|
253
|
+
} = analysis;
|
|
254
|
+
|
|
255
|
+
return [
|
|
256
|
+
`# Session Understanding Report`,
|
|
257
|
+
``,
|
|
258
|
+
`## Session Goal`,
|
|
259
|
+
``,
|
|
260
|
+
goal,
|
|
261
|
+
``,
|
|
262
|
+
`## Inputs`,
|
|
263
|
+
``,
|
|
264
|
+
`- Diff: \`${diffPath}\``,
|
|
265
|
+
`- Transcript: \`${transcriptPath}\``,
|
|
266
|
+
``,
|
|
267
|
+
`## What Changed`,
|
|
268
|
+
``,
|
|
269
|
+
`This session touched ${changedFiles.length || "no detected"} file${changedFiles.length === 1 ? "" : "s"} with ${stats.added} added line${stats.added === 1 ? "" : "s"} and ${stats.removed} removed line${stats.removed === 1 ? "" : "s"}.`,
|
|
270
|
+
``,
|
|
271
|
+
...formatChangedFiles(changedFiles),
|
|
272
|
+
``,
|
|
273
|
+
`## Problem Diagnosis`,
|
|
274
|
+
``,
|
|
275
|
+
buildProblemDiagnosis(goal, transcriptSignals),
|
|
276
|
+
``,
|
|
277
|
+
`## Meaningful Timeline`,
|
|
278
|
+
``,
|
|
279
|
+
...formatTimeline(transcriptSignals, changedFiles, stats),
|
|
280
|
+
``,
|
|
281
|
+
`## Decision Cards`,
|
|
282
|
+
``,
|
|
283
|
+
...formatDecisions(decisions),
|
|
284
|
+
``,
|
|
285
|
+
`## Risks And Edge Cases`,
|
|
286
|
+
``,
|
|
287
|
+
...formatRisks(risks),
|
|
288
|
+
``,
|
|
289
|
+
`## Alternative Approaches`,
|
|
290
|
+
``,
|
|
291
|
+
...formatAlternatives(decisions),
|
|
292
|
+
``,
|
|
293
|
+
`## Understanding Checklist`,
|
|
294
|
+
``,
|
|
295
|
+
...formatChecklist(decisions, risks),
|
|
296
|
+
``,
|
|
297
|
+
`## Quiz`,
|
|
298
|
+
``,
|
|
299
|
+
...formatQuiz(decisions, risks),
|
|
300
|
+
``,
|
|
301
|
+
`## Follow-Up Practice`,
|
|
302
|
+
``,
|
|
303
|
+
buildFollowUp(decisions, risks),
|
|
304
|
+
``,
|
|
305
|
+
`## Evidence Snippets`,
|
|
306
|
+
``,
|
|
307
|
+
...formatEvidence(diff, transcript)
|
|
308
|
+
].join("\n");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function extractChangedFiles(diff) {
|
|
312
|
+
const files = [];
|
|
313
|
+
for (const line of diff.split("\n")) {
|
|
314
|
+
const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
315
|
+
if (match) {
|
|
316
|
+
files.push(match[2]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return [...new Set(files)];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function summarizeDiff(diff) {
|
|
323
|
+
let added = 0;
|
|
324
|
+
let removed = 0;
|
|
325
|
+
|
|
326
|
+
for (const line of diff.split("\n")) {
|
|
327
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
328
|
+
if (line.startsWith("+")) added += 1;
|
|
329
|
+
if (line.startsWith("-")) removed += 1;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { added, removed };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function extractTranscriptSignals(transcript) {
|
|
336
|
+
const lines = transcript
|
|
337
|
+
.split("\n")
|
|
338
|
+
.map((line) => line.trim())
|
|
339
|
+
.filter(Boolean);
|
|
340
|
+
|
|
341
|
+
const signals = [];
|
|
342
|
+
for (const line of lines) {
|
|
343
|
+
if (/^(user|assistant|tool|command|test|error):/i.test(line)) {
|
|
344
|
+
signals.push(line);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return signals.slice(0, 12);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function extractDecisions({ diff, transcript }) {
|
|
352
|
+
const haystack = `${diff}\n${transcript}`;
|
|
353
|
+
const decisions = DECISION_RULES.filter((rule) =>
|
|
354
|
+
rule.patterns.some((pattern) => pattern.test(haystack))
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
if (decisions.length > 0) {
|
|
358
|
+
return decisions.slice(0, 5);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return [
|
|
362
|
+
{
|
|
363
|
+
id: "implementation-shape",
|
|
364
|
+
title: "Choose an implementation shape that matches the problem size",
|
|
365
|
+
why:
|
|
366
|
+
"The first engineering decision is whether the solution should be local and simple or abstracted for reuse.",
|
|
367
|
+
beginnerMiss:
|
|
368
|
+
"A beginner may copy the first working pattern without asking whether it fits the surrounding codebase.",
|
|
369
|
+
seniorCheck:
|
|
370
|
+
"A senior engineer would compare the change against existing project conventions and likely future requirements.",
|
|
371
|
+
alternatives: [
|
|
372
|
+
"Prototype: keep the change close to the use case.",
|
|
373
|
+
"Production-ready: extract a shared abstraction only after repeated usage is clear.",
|
|
374
|
+
"Overengineered: introduce a framework-level abstraction for a one-off behavior."
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function extractRisks({ diff, transcript }) {
|
|
381
|
+
const haystack = `${diff}\n${transcript}`;
|
|
382
|
+
const risks = [];
|
|
383
|
+
|
|
384
|
+
if (/SpeechRecognition|speechSynthesis|localStorage|window\./i.test(haystack)) {
|
|
385
|
+
risks.push("Browser capability: voice and storage APIs need unsupported-browser and permission-denied paths.");
|
|
386
|
+
}
|
|
387
|
+
if (/ANTHROPIC_API_KEY|Twilio|twilio|app\/api|NextRequest/i.test(haystack)) {
|
|
388
|
+
risks.push("Secret boundary: provider credentials must stay server-side and errors should not leak sensitive details.");
|
|
389
|
+
}
|
|
390
|
+
if (/goals\.json|fs\.writeFileSync|fs\.readFileSync|new Map|in-memory/i.test(haystack)) {
|
|
391
|
+
risks.push("Demo persistence: local files or memory will not survive serverless/runtime constraints or multi-user usage.");
|
|
392
|
+
}
|
|
393
|
+
if (/SAVE_GOALS|parseGoals|system prompt|MORNING_SYSTEM|EVENING_SYSTEM/i.test(haystack)) {
|
|
394
|
+
risks.push("Model protocol: parsing model text needs tests for malformed, missing, or unexpected structured markers.");
|
|
395
|
+
}
|
|
396
|
+
if (/password|unauthorized|reset token|resetToken/i.test(haystack)) {
|
|
397
|
+
risks.push("Security boundary: verify that unauthorized, expired, reused, or malformed credentials cannot succeed.");
|
|
398
|
+
}
|
|
399
|
+
if (/test|expect|assert|describe|it\(/i.test(haystack)) {
|
|
400
|
+
risks.push("Test coverage: check whether tests prove failure modes, not only the happy path.");
|
|
401
|
+
} else {
|
|
402
|
+
risks.push("Missing tests: this session does not show obvious regression coverage.");
|
|
403
|
+
}
|
|
404
|
+
if (/error|throw|catch|status|400|401|403|404|500/i.test(haystack)) {
|
|
405
|
+
risks.push("Failure semantics: make sure expected failures are distinguishable from unexpected system errors.");
|
|
406
|
+
}
|
|
407
|
+
if (/email|mail|notification/i.test(haystack)) {
|
|
408
|
+
risks.push("External side effect: email or notification behavior should be idempotent enough for retries.");
|
|
409
|
+
}
|
|
410
|
+
if (/db|database|sql|migration|schema|prisma|drizzle/i.test(haystack)) {
|
|
411
|
+
risks.push("Data model: confirm migration, rollback, and existing-data behavior.");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return [...new Set(risks)].slice(0, 6);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function buildProblemDiagnosis(goal, signals) {
|
|
418
|
+
const signalText = signals.length
|
|
419
|
+
? ` The transcript suggests the work moved through ${signals.length} notable prompt/tool moments.`
|
|
420
|
+
: "";
|
|
421
|
+
|
|
422
|
+
return `The session goal was to ${lowercaseFirst(goal)}. The important learning question is not only whether the implementation works, but what constraints shaped it, which choices created future maintenance obligations, and what evidence proves the behavior.${signalText}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function formatChangedFiles(files) {
|
|
426
|
+
if (files.length === 0) return ["No changed files were detected from the diff headers."];
|
|
427
|
+
return files.map((file) => `- \`${file}\``);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function formatTimeline(signals, files, stats) {
|
|
431
|
+
const timeline = [
|
|
432
|
+
`1. The human set the goal and the session established the intended behavior.`,
|
|
433
|
+
`2. The implementation changed ${files.length || "an unknown number of"} file${files.length === 1 ? "" : "s"}, creating the code evidence for review.`,
|
|
434
|
+
`3. The diff added ${stats.added} line${stats.added === 1 ? "" : "s"} and removed ${stats.removed} line${stats.removed === 1 ? "" : "s"}, which should be read for both behavior and tradeoffs.`
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
if (signals.length > 0) {
|
|
438
|
+
timeline.push(`4. The transcript contains notable moments such as: ${signals.slice(0, 3).join(" / ")}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return timeline;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function formatDecisions(decisions) {
|
|
445
|
+
return decisions.flatMap((decision, index) => [
|
|
446
|
+
`### ${index + 1}. ${decision.title}`,
|
|
447
|
+
``,
|
|
448
|
+
`**Why it mattered:** ${decision.why}`,
|
|
449
|
+
``,
|
|
450
|
+
`**What a beginner might miss:** ${decision.beginnerMiss}`,
|
|
451
|
+
``,
|
|
452
|
+
`**What a senior engineer would check:** ${decision.seniorCheck}`,
|
|
453
|
+
``
|
|
454
|
+
]);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function formatRisks(risks) {
|
|
458
|
+
return risks.map((risk) => `- ${risk}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function formatAlternatives(decisions) {
|
|
462
|
+
const alternatives = decisions.flatMap((decision) => decision.alternatives || []);
|
|
463
|
+
return [...new Set(alternatives)].slice(0, 8).map((alternative) => `- ${alternative}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function formatChecklist(decisions, risks) {
|
|
467
|
+
const checklist = decisions.map((decision) => `- [ ] Explain: ${lowercaseFirst(decision.title)}.`);
|
|
468
|
+
for (const risk of risks.slice(0, 3)) {
|
|
469
|
+
checklist.push(`- [ ] Identify how the implementation handles this risk: ${risk}`);
|
|
470
|
+
}
|
|
471
|
+
checklist.push("- [ ] Point to the exact code or test evidence that proves the intended behavior.");
|
|
472
|
+
return checklist;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function formatQuiz(decisions, risks) {
|
|
476
|
+
const questions = [];
|
|
477
|
+
const primary = decisions[0];
|
|
478
|
+
|
|
479
|
+
questions.push(`1. Why was "${primary.title}" an important decision in this session?`);
|
|
480
|
+
questions.push(`2. What is one alternative approach, and what tradeoff would it create?`);
|
|
481
|
+
questions.push(`3. Which edge case is most likely to break this implementation if it was missed?`);
|
|
482
|
+
|
|
483
|
+
if (risks.length > 0) {
|
|
484
|
+
questions.push(`4. What evidence would prove this risk is handled: ${risks[0]}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
questions.push("5. If you had to review this change tomorrow, what would you inspect first and why?");
|
|
488
|
+
return questions;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function buildFollowUp(decisions, risks) {
|
|
492
|
+
const decision = decisions[0];
|
|
493
|
+
const risk = risks[0] || "the most important failure mode";
|
|
494
|
+
return `Write one small test or example that would fail if "${decision.title}" was implemented incorrectly. Use it to prove the handling of ${risk}`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function formatEvidence(diff, transcript) {
|
|
498
|
+
return [
|
|
499
|
+
`### Diff`,
|
|
500
|
+
``,
|
|
501
|
+
"```diff",
|
|
502
|
+
...diff.split("\n").slice(0, MAX_SNIPPET_LINES),
|
|
503
|
+
"```",
|
|
504
|
+
``,
|
|
505
|
+
`### Transcript`,
|
|
506
|
+
``,
|
|
507
|
+
"```text",
|
|
508
|
+
...transcript.split("\n").slice(0, MAX_SNIPPET_LINES),
|
|
509
|
+
"```"
|
|
510
|
+
];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function lowercaseFirst(value) {
|
|
514
|
+
if (!value) return value;
|
|
515
|
+
return value.charAt(0).toLowerCase() + value.slice(1);
|
|
516
|
+
}
|