llm-mock-server 1.0.2 → 1.0.4

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 (51) hide show
  1. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +286 -0
  2. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +303 -0
  3. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +17 -0
  4. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +297 -0
  5. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +22 -0
  6. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +20 -0
  7. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +20 -0
  8. package/.desloppify/query.json +31 -103
  9. package/.desloppify/review_packet_blind.json +134 -188
  10. package/.desloppify/review_packets/holistic_packet_20260315_045546.json +1480 -0
  11. package/.desloppify/state-typescript.json +2285 -846
  12. package/.desloppify/state-typescript.json.bak +2252 -840
  13. package/.editorconfig +12 -0
  14. package/.github/workflows/test.yml +3 -0
  15. package/.oxfmtrc.json +9 -0
  16. package/README.md +5 -0
  17. package/package.json +5 -2
  18. package/scorecard.png +0 -0
  19. package/src/cli-validators.ts +12 -4
  20. package/src/cli.ts +25 -11
  21. package/src/formats/anthropic/parse.ts +24 -5
  22. package/src/formats/anthropic/schema.ts +16 -8
  23. package/src/formats/anthropic/serialize.ts +112 -28
  24. package/src/formats/openai/parse.ts +12 -2
  25. package/src/formats/openai/schema.ts +43 -30
  26. package/src/formats/openai/serialize.ts +73 -17
  27. package/src/formats/request-helpers.ts +2 -1
  28. package/src/formats/responses/parse.ts +17 -3
  29. package/src/formats/responses/schema.ts +34 -20
  30. package/src/formats/responses/serialize.ts +235 -40
  31. package/src/formats/serialize-helpers.ts +10 -2
  32. package/src/formats/types.ts +16 -3
  33. package/src/index.ts +3 -1
  34. package/src/loader.ts +48 -12
  35. package/src/logger.ts +25 -7
  36. package/src/mock-server.ts +28 -7
  37. package/src/route-handler.ts +49 -14
  38. package/src/rule-engine.ts +43 -12
  39. package/src/types/reply.ts +6 -2
  40. package/src/types.ts +24 -3
  41. package/test/cli-validators.test.ts +16 -4
  42. package/test/formats/anthropic.test.ts +95 -19
  43. package/test/formats/openai.test.ts +85 -24
  44. package/test/formats/parse-helpers.test.ts +47 -7
  45. package/test/formats/responses.test.ts +111 -30
  46. package/test/history.test.ts +18 -5
  47. package/test/loader.test.ts +52 -17
  48. package/test/logger.test.ts +59 -9
  49. package/test/mock-server.test.ts +76 -22
  50. package/test/rule-engine.test.ts +49 -19
  51. /package/{ARCHITECTURE.md → docs/ARCHITECTURE.md} +0 -0
@@ -0,0 +1,286 @@
1
+ {
2
+ "assessments": {
3
+ "cross_module_architecture": 93.0,
4
+ "convention_outlier": 90.0,
5
+ "error_consistency": 82.0,
6
+ "abstraction_fitness": 91.0,
7
+ "api_surface_coherence": 85.0,
8
+ "authorization_consistency": 100.0,
9
+ "ai_generated_debt": 88.0,
10
+ "incomplete_migration": 95.0,
11
+ "package_organization": 94.0,
12
+ "high_level_elegance": 92.0,
13
+ "mid_level_elegance": 88.0,
14
+ "low_level_elegance": 86.0,
15
+ "design_coherence": 87.0
16
+ },
17
+ "findings": [
18
+ {
19
+ "dimension": "error_consistency",
20
+ "identifier": "resolver_error_swallowed_silently",
21
+ "summary": "Resolver errors are logged then silently replaced with fallback, losing error context for callers",
22
+ "related_files": [
23
+ "src/route-handler.ts"
24
+ ],
25
+ "evidence": [
26
+ "In resolveReply() (lines 36-46), when matched.resolve throws, the error is caught, logged, and the fallback reply is returned. The caller has no way to distinguish a successful fallback from a resolver failure. History records the rule description but not the error state, making debugging difficult in tests."
27
+ ],
28
+ "suggestion": "Record the error state in the history entry (e.g. add an `error` field to RecordedRequest), or at minimum set a distinct ruleDesc like `${matched.description} (error)` so that test assertions can detect resolver failures vs. normal fallback usage.",
29
+ "confidence": "medium"
30
+ },
31
+ {
32
+ "dimension": "error_consistency",
33
+ "identifier": "history_recorded_before_streaming_completes",
34
+ "summary": "Error reply records history before send, but streaming path also records before writeSSE may fail",
35
+ "related_files": [
36
+ "src/route-handler.ts"
37
+ ],
38
+ "evidence": [
39
+ "Line 109 and 116: history.record() is called before the response is fully sent. For error replies (line 109) this is fine, but for the streaming path (line 116), if writeSSE throws (e.g. client disconnect), the request is recorded as successfully handled. The recording point is inconsistent between error and success paths -- error records at line 109 then returns, normal records at 116 then may stream or return JSON."
40
+ ],
41
+ "suggestion": "Move history.record() after the response is fully sent (after writeSSE completes for streaming, after reply.send for JSON), or add a status/success field to the recorded entry. This ensures history reflects actual outcome.",
42
+ "confidence": "medium"
43
+ },
44
+ {
45
+ "dimension": "error_consistency",
46
+ "identifier": "loader_silent_skip_on_unknown_extension",
47
+ "summary": "loadRulesFromPath silently skips files with unrecognized extensions instead of warning",
48
+ "related_files": [
49
+ "src/loader.ts"
50
+ ],
51
+ "evidence": [
52
+ "Lines 222-225: When a file path is given with an extension not in loaderByExtension (e.g. '.yaml', '.txt'), the function silently returns without loading anything or signaling an issue. The directory-loading path (line 234) also calls loadRulesFromPath recursively, so stray files in a rules directory are silently ignored."
53
+ ],
54
+ "suggestion": "When loading a single file (info.isFile()) with an unsupported extension, either throw an error ('Unsupported file extension...') or accept a Logger parameter and log a warning. Silent skipping is surprising when a user explicitly passes a file path.",
55
+ "confidence": "high"
56
+ },
57
+ {
58
+ "dimension": "convention_outlier",
59
+ "identifier": "buildUsage_duplicated_across_serializers",
60
+ "summary": "buildUsage helper is independently defined in all three format serializers with slightly different shapes",
61
+ "related_files": [
62
+ "src/formats/openai/serialize.ts",
63
+ "src/formats/anthropic/serialize.ts",
64
+ "src/formats/responses/serialize.ts"
65
+ ],
66
+ "evidence": [
67
+ "OpenAI buildUsage (line 12) returns {prompt_tokens, completion_tokens, total_tokens, ..._details}. Anthropic buildUsage (line 12) returns {input_tokens, output_tokens}. Responses buildUsage (line 12) returns {input_tokens, output_tokens, total_tokens}. Each file defines its own private buildUsage function with the same name, same input signature ({input: number, output: number}), but different output shapes. This is a sibling behavioral inconsistency -- three siblings all do the same conceptual transformation but aren't coordinated."
68
+ ],
69
+ "suggestion": "This is intentional variation (different API formats require different shapes), so no structural change is needed, but consider renaming the functions to be more specific (e.g. buildOpenAIUsage, buildAnthropicUsage) or adding a brief comment noting the format-specific shape is deliberate. The identical naming across three files creates a false sense of fungibility.",
70
+ "confidence": "low"
71
+ },
72
+ {
73
+ "dimension": "api_surface_coherence",
74
+ "identifier": "cli_validators_mixed_sync_async",
75
+ "summary": "parseHost is async while all sibling validators (parsePort, parseLogLevel, etc.) are sync",
76
+ "related_files": [
77
+ "src/cli-validators.ts"
78
+ ],
79
+ "evidence": [
80
+ "parsePort (line 13), parseLogLevel (line 21), parseChunkSize (line 49), parseLatency (line 59) are all synchronous functions. parseHost (line 30) is async because it calls dns.lookup. This forces the caller (cli.ts line 38) to await parseHost while the others are called synchronously. The mixed sync/async surface in a cohesive set of validation functions is surprising."
81
+ ],
82
+ "suggestion": "Consider making parseHost synchronous by using isIP() for IP addresses and a regex for valid hostname format, deferring actual resolution to the server's listen() call. If DNS validation is essential, document in parseHost's JSDoc that it is async unlike its siblings, and consider grouping it separately.",
83
+ "confidence": "medium"
84
+ },
85
+ {
86
+ "dimension": "api_surface_coherence",
87
+ "identifier": "isStreaming_default_true_surprising",
88
+ "summary": "isStreaming defaults to true for any non-object input including null/undefined, which is a surprising API contract",
89
+ "related_files": [
90
+ "src/formats/request-helpers.ts"
91
+ ],
92
+ "evidence": [
93
+ "isStreaming (line 9-11) returns `asRecord(body)['stream'] !== false`, meaning any body that isn't an object with `stream: false` is treated as streaming. Passing null, undefined, a string, or a number all return true. This is an implicit opt-out contract rather than opt-in, which can silently produce streaming responses when the caller expected JSON."
94
+ ],
95
+ "suggestion": "Consider making streaming explicit: return true only when stream is explicitly true or when it's a valid object without a stream field. At minimum, add a JSDoc comment explaining the opt-out default. The current behavior is consistent with some LLM API defaults but could be surprising to mock server users.",
96
+ "confidence": "low"
97
+ },
98
+ {
99
+ "dimension": "low_level_elegance",
100
+ "identifier": "genId_collision_risk",
101
+ "summary": "genId uses Date.now() in base36, creating collision risk for IDs generated in the same millisecond",
102
+ "related_files": [
103
+ "src/formats/serialize-helpers.ts"
104
+ ],
105
+ "evidence": [
106
+ "genId (line 16-18) generates IDs as `${prefix}_${Date.now().toString(36)}`. Two calls within the same millisecond produce identical IDs. This affects toolId (line 20-26) as well -- multiple tools in the same reply will get IDs that differ only by the index suffix, but if toolId is called without an index (or with 0 for multiple tools), IDs collide. In serializeComplete for Anthropic (line 152), all tools use index 0: `toolId(tool, 'toolu', 0)` causing collisions when multiple tools are present."
107
+ ],
108
+ "suggestion": "Add a monotonic counter or random suffix to genId: `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`. Alternatively, use crypto.randomUUID() or nanoid (already a transitive dependency). Also fix the Anthropic serializeComplete to pass the actual tool index instead of hardcoded 0.",
109
+ "confidence": "high"
110
+ },
111
+ {
112
+ "dimension": "low_level_elegance",
113
+ "identifier": "anthropic_serializeComplete_tool_index_hardcoded",
114
+ "summary": "Anthropic serializeComplete passes hardcoded index 0 for all tool IDs, causing ID collisions",
115
+ "related_files": [
116
+ "src/formats/anthropic/serialize.ts"
117
+ ],
118
+ "evidence": [
119
+ "Line 152: `(reply.tools ?? []).map((tool) => ({ ... id: toolId(tool, 'toolu', 0), ... }))` -- every tool in the array gets index 0 passed to toolId. When multiple tools are present and none have explicit IDs, they all get the same generated ID (same prefix, same timestamp, same index). The streaming path (toolBlocks, line 65) correctly uses `startIndex + i` for each tool."
120
+ ],
121
+ "suggestion": "Change line 152 to use the map index: `(reply.tools ?? []).map((tool, i) => ({ ... id: toolId(tool, 'toolu', i), ... }))`",
122
+ "confidence": "high"
123
+ },
124
+ {
125
+ "dimension": "low_level_elegance",
126
+ "identifier": "responses_serializeComplete_tool_index_hardcoded",
127
+ "summary": "Responses serializeComplete also uses hardcoded index 0 for all tool call IDs",
128
+ "related_files": [
129
+ "src/formats/responses/serialize.ts"
130
+ ],
131
+ "evidence": [
132
+ "Lines 291-300: `(reply.tools ?? []).map((tool) => { const callId = toolId(tool, 'call', 0); ... })` -- same issue as Anthropic. When multiple tools lack explicit IDs, they all get the same generated ID. The streaming path (toolStreamBlock) correctly uses the incrementing `i` variable."
133
+ ],
134
+ "suggestion": "Change to use the map index: `(reply.tools ?? []).map((tool, i) => { const callId = toolId(tool, 'call', i); ... })`",
135
+ "confidence": "high"
136
+ },
137
+ {
138
+ "dimension": "mid_level_elegance",
139
+ "identifier": "sequence_resolver_mutates_rule_options",
140
+ "summary": "createSequenceResolver mutates rule.options on each call, creating a side-channel between the resolver and the route handler",
141
+ "related_files": [
142
+ "src/rule-engine.ts",
143
+ "src/route-handler.ts"
144
+ ],
145
+ "evidence": [
146
+ "In createSequenceResolver (lines 91-107), the returned resolver function mutates `rule.options` on each invocation (line 102: `rule.options = step.options ?? {}`). In route-handler.ts line 119, `effectiveOptions` is computed as `{ ...defaultOptions, ...matched?.options }` AFTER resolveReply has already run. This works because resolveReply calls matched.resolve which mutates matched.options before effectiveOptions is read. But this temporal coupling is fragile -- the reader must understand that resolveReply has a side effect on matched.options that the subsequent line depends on."
147
+ ],
148
+ "suggestion": "Return the per-step options from the resolver instead of mutating the rule object. For example, have the resolver return `{ reply, options }` and let resolveReply propagate both values. This makes the data flow explicit rather than relying on mutation timing.",
149
+ "confidence": "medium"
150
+ },
151
+ {
152
+ "dimension": "design_coherence",
153
+ "identifier": "mock_server_when_builds_rule_handle_inline",
154
+ "summary": "MockServer.when() constructs PendingRule and RuleHandle inline with closure-captured state, mixing builder logic into the server",
155
+ "related_files": [
156
+ "src/mock-server.ts"
157
+ ],
158
+ "evidence": [
159
+ "Lines 103-133: The when() method constructs a PendingRule object with reply() and replySequence() methods inline. The makeHandle closure creates RuleHandle objects. The replySequence method (lines 121-132) duplicates logic from the loader's addSequenceRule (loader.ts lines 108-131) -- both normalize sequence entries, call engine.add, then call createSequenceResolver and patch the rule. This is two implementations of the same concept."
160
+ ],
161
+ "suggestion": "Extract the sequence-rule creation into a shared function (e.g. in rule-engine.ts: `addSequenceRule(engine, match, steps)`) and call it from both MockServer.when().replySequence() and loader.ts addSequenceRule(). This eliminates the duplicated normalization and rule-patching logic.",
162
+ "confidence": "medium"
163
+ },
164
+ {
165
+ "dimension": "design_coherence",
166
+ "identifier": "route_handler_function_does_too_much",
167
+ "summary": "createRouteHandler's returned handler function performs parsing, matching, resolving, error handling, streaming decision, and response sending",
168
+ "related_files": [
169
+ "src/route-handler.ts"
170
+ ],
171
+ "evidence": [
172
+ "The handler function (lines 63-147) is 85 lines and handles: (1) header extraction, (2) request parsing with Zod validation, (3) rule matching, (4) reply resolution, (5) error reply handling, (6) history recording, (7) streaming vs. non-streaming decision, (8) logging, (9) SSE writing. While each step is relatively straightforward, the single function accumulates all orchestration responsibilities."
173
+ ],
174
+ "suggestion": "The function is cohesive enough that splitting it could be worse than the status quo. However, consider extracting the header-extraction and request-parsing into a helper (e.g. `parseIncomingRequest(format, request)`) to reduce the cognitive load of the main handler. The current structure is functional but approaches the point where a new engineer would need to read the entire function to understand any part of it.",
175
+ "confidence": "low"
176
+ },
177
+ {
178
+ "dimension": "ai_generated_debt",
179
+ "identifier": "type_files_high_comment_ratio",
180
+ "summary": "Type definition files have disproportionately high comment ratios (31-32%) relative to codebase average (4.2%)",
181
+ "related_files": [
182
+ "src/types/request.ts",
183
+ "src/types/rule.ts"
184
+ ],
185
+ "evidence": [
186
+ "src/types/request.ts has 32% comment ratio vs 4.2% codebase average. src/types/rule.ts has 31% comment ratio. Many comments restate what the type signature already communicates. For example, in request.ts: `readonly lastMessage: string` has comment `/** The last user message's text. This is what most matchers check. */` -- the second sentence adds value but the first sentence restates the name. In rule.ts: `/** Returned by `when()`. Call `.reply()` or `.replySequence()` on it to complete the rule. */` on PendingRule -- the interface definition below makes this obvious."
187
+ ],
188
+ "suggestion": "Trim JSDoc comments on types to only include non-obvious information. Keep comments that explain 'why' or usage guidance (like 'This is what most matchers check'), but remove comments that restate the type name or signature. For example, `readonly tools?: readonly ToolDef[] | undefined;` needs no doc comment; the name and type are self-documenting.",
189
+ "confidence": "medium"
190
+ },
191
+ {
192
+ "dimension": "incomplete_migration",
193
+ "identifier": "single_require_in_esm_codebase",
194
+ "summary": "cli.ts uses createRequire to read package.json version, the only require() in an otherwise pure ESM codebase",
195
+ "related_files": [
196
+ "src/cli.ts"
197
+ ],
198
+ "evidence": [
199
+ "Line 4: `import { createRequire } from 'node:module'` and line 17: `const require = createRequire(import.meta.url); const { version } = require('../package.json') as { version: string };`. The entire codebase is ESM (type: module in package.json, all other imports use ESM syntax). This is the sole use of require()."
200
+ ],
201
+ "suggestion": "Use an import assertion/attribute to load the package.json: `import pkg from '../package.json' with { type: 'json' };` (Node 22+ supports this). This eliminates the require shim and makes the codebase fully ESM.",
202
+ "confidence": "medium"
203
+ },
204
+ {
205
+ "dimension": "cross_module_architecture",
206
+ "identifier": "dynamic_import_of_loader_in_server",
207
+ "summary": "MockServer.load() uses a dynamic import for loader.js, creating a hidden runtime dependency that static analysis cannot trace",
208
+ "related_files": [
209
+ "src/mock-server.ts",
210
+ "src/loader.ts"
211
+ ],
212
+ "evidence": [
213
+ "Line 187: `const { loadRulesFromPath } = await import('./loader.js');`. This dynamic import means loader.ts is not in the static dependency graph of mock-server.ts. While this may be intentional for tree-shaking (users who never call load() don't pay for the loader code), it creates an invisible architecture edge that tools and developers cannot see without reading the implementation."
214
+ ],
215
+ "suggestion": "Add a comment explaining the intent (tree-shaking/code-splitting). If tree-shaking is not a goal, convert to a static import. If it is intentional, document this in the module's JSDoc: `/** Dynamically imports loader.js to keep it out of the bundle for users who don't use file-based rules. */`",
216
+ "confidence": "medium"
217
+ },
218
+ {
219
+ "dimension": "mid_level_elegance",
220
+ "identifier": "cli_watch_debounce_races",
221
+ "summary": "CLI watch mode uses a boolean flag for debouncing that can miss rapid successive changes",
222
+ "related_files": [
223
+ "src/cli.ts"
224
+ ],
225
+ "evidence": [
226
+ "Lines 88-103: The watch handler uses a `reloading` boolean flag and setTimeout with WATCH_DEBOUNCE_MS=100. If a change fires while reloading is true (during the 100ms timeout or during the async reload), it is silently dropped. A rapid sequence of saves could result in the first change being loaded and the last (most current) being missed entirely, leaving stale rules loaded."
227
+ ],
228
+ "suggestion": "Use a proper debounce pattern that resets the timer on each new event, ensuring the final change is always processed: store the timeout ID and clear/reset it on each watch callback. Example: `let timer: NodeJS.Timeout | undefined; watch(..., () => { clearTimeout(timer); timer = setTimeout(async () => { ... }, WATCH_DEBOUNCE_MS); });`",
229
+ "confidence": "high"
230
+ },
231
+ {
232
+ "dimension": "high_level_elegance",
233
+ "identifier": "format_interface_serializeError_status_unused",
234
+ "summary": "Format.serializeError receives status but all three implementations ignore it in the response body",
235
+ "related_files": [
236
+ "src/formats/types.ts",
237
+ "src/formats/openai/serialize.ts",
238
+ "src/formats/anthropic/serialize.ts",
239
+ "src/formats/responses/serialize.ts"
240
+ ],
241
+ "evidence": [
242
+ "The Format interface (types.ts line 25) defines serializeError with `status: number` in the parameter. But all three implementations ignore the status field when constructing the error body -- OpenAI (serialize.ts line 134-146) only uses message and type, Anthropic (serialize.ts line 170-179) only uses message and type, Responses (serialize.ts line 315-328) only uses message and type. The status is used by route-handler.ts (line 113) to set the HTTP status code, not by the serializer. The parameter creates a false expectation that the serializer should use it."
243
+ ],
244
+ "suggestion": "Remove `status` from the serializeError parameter signature in the Format interface and all implementations, since it is only used by the route handler for the HTTP status code, not by the serialization logic. This makes the boundary clearer.",
245
+ "confidence": "medium"
246
+ },
247
+ {
248
+ "dimension": "abstraction_fitness",
249
+ "identifier": "buildMockRequest_seven_params",
250
+ "summary": "buildMockRequest takes 7 positional parameters, making call sites hard to read",
251
+ "related_files": [
252
+ "src/formats/request-helpers.ts"
253
+ ],
254
+ "evidence": [
255
+ "buildMockRequest (line 25-51) accepts: format, body, messages, tools, defaultModel, raw, meta. All three callers (openai/parse.ts:35, anthropic/parse.ts:58, responses/parse.ts:61) pass 7 positional arguments. The call sites read like `buildMockRequest('openai', req, parseMessages(req), parseTools(req), 'gpt-5.4', body, meta)` which requires counting positions to understand what each argument is."
256
+ ],
257
+ "suggestion": "Group the parameters into an options object: `buildMockRequest({ format: 'openai', body: req, messages: parseMessages(req), tools: parseTools(req), defaultModel: 'gpt-5.4', raw: body, meta })`. This is a common pattern for functions with more than 4-5 parameters and makes call sites self-documenting.",
258
+ "confidence": "medium"
259
+ }
260
+ ],
261
+ "dimension_notes": {
262
+ "authorization_consistency": "This is a mock server intended for local testing. Routes are intentionally unauthenticated by design -- the server's purpose is to simulate LLM API responses in test environments. No auth gaps exist because auth is not part of the domain.",
263
+ "cross_module_architecture": "The codebase has clean dependency direction: types flow outward, formats depend on shared helpers, mock-server orchestrates. The one notable edge is the dynamic import of loader.js, which is reported as a finding. Otherwise, boundaries are well-defined.",
264
+ "convention_outlier": "The three format modules (openai, anthropic, responses) follow an identical file structure (index.ts, parse.ts, schema.ts, serialize.ts) which is excellent consistency. Minor outlier is the duplicated buildUsage function naming across serializers.",
265
+ "incomplete_migration": "Nearly fully ESM. The single createRequire usage in cli.ts is the only CJS residue.",
266
+ "package_organization": "For a project of this size (~40 files), the directory layout is well-organized with clear domain boundaries. The formats/ subdirectory with per-provider modules is clean.",
267
+ "high_level_elegance": "The decomposition is clear: types define contracts, formats handle protocol translation, rule-engine handles matching, mock-server orchestrates. The Format interface provides a clean plugin boundary. Minor concern about serializeError parameter mismatch is reported.",
268
+ "abstraction_fitness": "Abstractions are generally well-fitted. The Format interface earns its keep across three implementations. The buildMockRequest parameter count is the main concern.",
269
+ "api_surface_coherence": "The public API (MockServer class) is coherent. The mixed sync/async in cli-validators is the notable inconsistency.",
270
+ "mid_level_elegance": "Handoffs between modules are mostly clean. The sequence resolver mutation and the CLI watch debounce pattern are the notable rough edges.",
271
+ "low_level_elegance": "Implementation craft is generally good. The ID generation collision risk and the hardcoded tool index in serializeComplete are the main issues.",
272
+ "design_coherence": "Functions are mostly focused. The duplicated sequence-rule logic between MockServer.when() and loader.ts is the clearest design coherence issue.",
273
+ "ai_generated_debt": "The codebase is generally clean of AI-generated patterns. The type files have somewhat elevated comment ratios with some restating comments, but the comments do generally add value.",
274
+ "error_consistency": "Error handling is functional but inconsistent in a few places: resolver errors are silently swallowed with fallback, unknown file extensions are silently skipped, and history recording timing is inconsistent between error and success paths."
275
+ },
276
+ "provenance": {
277
+ "kind": "blind_review_batch_import",
278
+ "blind": true,
279
+ "runner": "claude",
280
+ "run_stamp": "ext_20260315_045546_0587ea3b",
281
+ "created_at": "2026-03-15T05:00:00+00:00",
282
+ "packet_path": "/Users/suyash.x.srijan/Documents/Personal_Projects/llm-mock-server/.desloppify/review_packet_blind.json",
283
+ "packet_sha256": "1405a7ac30145db0e952bfb0b7abd92e594e863c7dd65c60dadbfe28680cf423",
284
+ "external_session_id": "ext_20260315_045546_0587ea3b"
285
+ }
286
+ }
@@ -0,0 +1,303 @@
1
+ {
2
+ "assessments": {
3
+ "cross_module_architecture": 93.0,
4
+ "convention_outlier": 90.0,
5
+ "error_consistency": 82.0,
6
+ "abstraction_fitness": 91.0,
7
+ "api_surface_coherence": 85.0,
8
+ "authorization_consistency": 100.0,
9
+ "ai_generated_debt": 88.0,
10
+ "incomplete_migration": 95.0,
11
+ "package_organization": 94.0,
12
+ "high_level_elegance": 92.0,
13
+ "mid_level_elegance": 88.0,
14
+ "low_level_elegance": 86.0,
15
+ "design_coherence": 87.0
16
+ },
17
+ "findings": [
18
+ {
19
+ "dimension": "error_consistency",
20
+ "identifier": "resolver_error_swallowed_silently",
21
+ "summary": "Resolver errors are logged then silently replaced with fallback, losing error context for callers",
22
+ "related_files": [
23
+ "src/route-handler.ts"
24
+ ],
25
+ "evidence": [
26
+ "In resolveReply() (lines 36-46), when matched.resolve throws, the error is caught, logged, and the fallback reply is returned. The caller has no way to distinguish a successful fallback from a resolver failure. History records the rule description but not the error state, making debugging difficult in tests."
27
+ ],
28
+ "suggestion": "Record the error state in the history entry (e.g. add an `error` field to RecordedRequest), or at minimum set a distinct ruleDesc like `${matched.description} (error)` so that test assertions can detect resolver failures vs. normal fallback usage.",
29
+ "confidence": "medium"
30
+ },
31
+ {
32
+ "dimension": "error_consistency",
33
+ "identifier": "history_recorded_before_streaming_completes",
34
+ "summary": "Error reply records history before send, but streaming path also records before writeSSE may fail",
35
+ "related_files": [
36
+ "src/route-handler.ts"
37
+ ],
38
+ "evidence": [
39
+ "Line 109 and 116: history.record() is called before the response is fully sent. For error replies (line 109) this is fine, but for the streaming path (line 116), if writeSSE throws (e.g. client disconnect), the request is recorded as successfully handled. The recording point is inconsistent between error and success paths -- error records at line 109 then returns, normal records at 116 then may stream or return JSON."
40
+ ],
41
+ "suggestion": "Move history.record() after the response is fully sent (after writeSSE completes for streaming, after reply.send for JSON), or add a status/success field to the recorded entry. This ensures history reflects actual outcome.",
42
+ "confidence": "medium"
43
+ },
44
+ {
45
+ "dimension": "error_consistency",
46
+ "identifier": "loader_silent_skip_on_unknown_extension",
47
+ "summary": "loadRulesFromPath silently skips files with unrecognized extensions instead of warning",
48
+ "related_files": [
49
+ "src/loader.ts"
50
+ ],
51
+ "evidence": [
52
+ "Lines 222-225: When a file path is given with an extension not in loaderByExtension (e.g. '.yaml', '.txt'), the function silently returns without loading anything or signaling an issue. The directory-loading path (line 234) also calls loadRulesFromPath recursively, so stray files in a rules directory are silently ignored."
53
+ ],
54
+ "suggestion": "When loading a single file (info.isFile()) with an unsupported extension, either throw an error ('Unsupported file extension...') or accept a Logger parameter and log a warning. Silent skipping is surprising when a user explicitly passes a file path.",
55
+ "confidence": "high"
56
+ },
57
+ {
58
+ "dimension": "convention_outlier",
59
+ "identifier": "buildUsage_duplicated_across_serializers",
60
+ "summary": "buildUsage helper is independently defined in all three format serializers with slightly different shapes",
61
+ "related_files": [
62
+ "src/formats/openai/serialize.ts",
63
+ "src/formats/anthropic/serialize.ts",
64
+ "src/formats/responses/serialize.ts"
65
+ ],
66
+ "evidence": [
67
+ "OpenAI buildUsage (line 12) returns {prompt_tokens, completion_tokens, total_tokens, ..._details}. Anthropic buildUsage (line 12) returns {input_tokens, output_tokens}. Responses buildUsage (line 12) returns {input_tokens, output_tokens, total_tokens}. Each file defines its own private buildUsage function with the same name, same input signature ({input: number, output: number}), but different output shapes. This is a sibling behavioral inconsistency -- three siblings all do the same conceptual transformation but aren't coordinated."
68
+ ],
69
+ "suggestion": "This is intentional variation (different API formats require different shapes), so no structural change is needed, but consider renaming the functions to be more specific (e.g. buildOpenAIUsage, buildAnthropicUsage) or adding a brief comment noting the format-specific shape is deliberate. The identical naming across three files creates a false sense of fungibility.",
70
+ "confidence": "low"
71
+ },
72
+ {
73
+ "dimension": "api_surface_coherence",
74
+ "identifier": "cli_validators_mixed_sync_async",
75
+ "summary": "parseHost is async while all sibling validators (parsePort, parseLogLevel, etc.) are sync",
76
+ "related_files": [
77
+ "src/cli-validators.ts"
78
+ ],
79
+ "evidence": [
80
+ "parsePort (line 13), parseLogLevel (line 21), parseChunkSize (line 49), parseLatency (line 59) are all synchronous functions. parseHost (line 30) is async because it calls dns.lookup. This forces the caller (cli.ts line 38) to await parseHost while the others are called synchronously. The mixed sync/async surface in a cohesive set of validation functions is surprising."
81
+ ],
82
+ "suggestion": "Consider making parseHost synchronous by using isIP() for IP addresses and a regex for valid hostname format, deferring actual resolution to the server's listen() call. If DNS validation is essential, document in parseHost's JSDoc that it is async unlike its siblings, and consider grouping it separately.",
83
+ "confidence": "medium"
84
+ },
85
+ {
86
+ "dimension": "api_surface_coherence",
87
+ "identifier": "isStreaming_default_true_surprising",
88
+ "summary": "isStreaming defaults to true for any non-object input including null/undefined, which is a surprising API contract",
89
+ "related_files": [
90
+ "src/formats/request-helpers.ts"
91
+ ],
92
+ "evidence": [
93
+ "isStreaming (line 9-11) returns `asRecord(body)['stream'] !== false`, meaning any body that isn't an object with `stream: false` is treated as streaming. Passing null, undefined, a string, or a number all return true. This is an implicit opt-out contract rather than opt-in, which can silently produce streaming responses when the caller expected JSON."
94
+ ],
95
+ "suggestion": "Consider making streaming explicit: return true only when stream is explicitly true or when it's a valid object without a stream field. At minimum, add a JSDoc comment explaining the opt-out default. The current behavior is consistent with some LLM API defaults but could be surprising to mock server users.",
96
+ "confidence": "low"
97
+ },
98
+ {
99
+ "dimension": "low_level_elegance",
100
+ "identifier": "genId_collision_risk",
101
+ "summary": "genId uses Date.now() in base36, creating collision risk for IDs generated in the same millisecond",
102
+ "related_files": [
103
+ "src/formats/serialize-helpers.ts"
104
+ ],
105
+ "evidence": [
106
+ "genId (line 16-18) generates IDs as `${prefix}_${Date.now().toString(36)}`. Two calls within the same millisecond produce identical IDs. This affects toolId (line 20-26) as well -- multiple tools in the same reply will get IDs that differ only by the index suffix, but if toolId is called without an index (or with 0 for multiple tools), IDs collide. In serializeComplete for Anthropic (line 152), all tools use index 0: `toolId(tool, 'toolu', 0)` causing collisions when multiple tools are present."
107
+ ],
108
+ "suggestion": "Add a monotonic counter or random suffix to genId: `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`. Alternatively, use crypto.randomUUID() or nanoid (already a transitive dependency). Also fix the Anthropic serializeComplete to pass the actual tool index instead of hardcoded 0.",
109
+ "confidence": "high"
110
+ },
111
+ {
112
+ "dimension": "low_level_elegance",
113
+ "identifier": "anthropic_serializeComplete_tool_index_hardcoded",
114
+ "summary": "Anthropic serializeComplete passes hardcoded index 0 for all tool IDs, causing ID collisions",
115
+ "related_files": [
116
+ "src/formats/anthropic/serialize.ts"
117
+ ],
118
+ "evidence": [
119
+ "Line 152: `(reply.tools ?? []).map((tool) => ({ ... id: toolId(tool, 'toolu', 0), ... }))` -- every tool in the array gets index 0 passed to toolId. When multiple tools are present and none have explicit IDs, they all get the same generated ID (same prefix, same timestamp, same index). The streaming path (toolBlocks, line 65) correctly uses `startIndex + i` for each tool."
120
+ ],
121
+ "suggestion": "Change line 152 to use the map index: `(reply.tools ?? []).map((tool, i) => ({ ... id: toolId(tool, 'toolu', i), ... }))`",
122
+ "confidence": "high"
123
+ },
124
+ {
125
+ "dimension": "low_level_elegance",
126
+ "identifier": "responses_serializeComplete_tool_index_hardcoded",
127
+ "summary": "Responses serializeComplete also uses hardcoded index 0 for all tool call IDs",
128
+ "related_files": [
129
+ "src/formats/responses/serialize.ts"
130
+ ],
131
+ "evidence": [
132
+ "Lines 291-300: `(reply.tools ?? []).map((tool) => { const callId = toolId(tool, 'call', 0); ... })` -- same issue as Anthropic. When multiple tools lack explicit IDs, they all get the same generated ID. The streaming path (toolStreamBlock) correctly uses the incrementing `i` variable."
133
+ ],
134
+ "suggestion": "Change to use the map index: `(reply.tools ?? []).map((tool, i) => { const callId = toolId(tool, 'call', i); ... })`",
135
+ "confidence": "high"
136
+ },
137
+ {
138
+ "dimension": "mid_level_elegance",
139
+ "identifier": "sequence_resolver_mutates_rule_options",
140
+ "summary": "createSequenceResolver mutates rule.options on each call, creating a side-channel between the resolver and the route handler",
141
+ "related_files": [
142
+ "src/rule-engine.ts",
143
+ "src/route-handler.ts"
144
+ ],
145
+ "evidence": [
146
+ "In createSequenceResolver (lines 91-107), the returned resolver function mutates `rule.options` on each invocation (line 102: `rule.options = step.options ?? {}`). In route-handler.ts line 119, `effectiveOptions` is computed as `{ ...defaultOptions, ...matched?.options }` AFTER resolveReply has already run. This works because resolveReply calls matched.resolve which mutates matched.options before effectiveOptions is read. But this temporal coupling is fragile -- the reader must understand that resolveReply has a side effect on matched.options that the subsequent line depends on."
147
+ ],
148
+ "suggestion": "Return the per-step options from the resolver instead of mutating the rule object. For example, have the resolver return `{ reply, options }` and let resolveReply propagate both values. This makes the data flow explicit rather than relying on mutation timing.",
149
+ "confidence": "medium"
150
+ },
151
+ {
152
+ "dimension": "design_coherence",
153
+ "identifier": "mock_server_when_builds_rule_handle_inline",
154
+ "summary": "MockServer.when() constructs PendingRule and RuleHandle inline with closure-captured state, mixing builder logic into the server",
155
+ "related_files": [
156
+ "src/mock-server.ts"
157
+ ],
158
+ "evidence": [
159
+ "Lines 103-133: The when() method constructs a PendingRule object with reply() and replySequence() methods inline. The makeHandle closure creates RuleHandle objects. The replySequence method (lines 121-132) duplicates logic from the loader's addSequenceRule (loader.ts lines 108-131) -- both normalize sequence entries, call engine.add, then call createSequenceResolver and patch the rule. This is two implementations of the same concept."
160
+ ],
161
+ "suggestion": "Extract the sequence-rule creation into a shared function (e.g. in rule-engine.ts: `addSequenceRule(engine, match, steps)`) and call it from both MockServer.when().replySequence() and loader.ts addSequenceRule(). This eliminates the duplicated normalization and rule-patching logic.",
162
+ "confidence": "medium"
163
+ },
164
+ {
165
+ "dimension": "design_coherence",
166
+ "identifier": "route_handler_function_does_too_much",
167
+ "summary": "createRouteHandler's returned handler function performs parsing, matching, resolving, error handling, streaming decision, and response sending",
168
+ "related_files": [
169
+ "src/route-handler.ts"
170
+ ],
171
+ "evidence": [
172
+ "The handler function (lines 63-147) is 85 lines and handles: (1) header extraction, (2) request parsing with Zod validation, (3) rule matching, (4) reply resolution, (5) error reply handling, (6) history recording, (7) streaming vs. non-streaming decision, (8) logging, (9) SSE writing. While each step is relatively straightforward, the single function accumulates all orchestration responsibilities."
173
+ ],
174
+ "suggestion": "The function is cohesive enough that splitting it could be worse than the status quo. However, consider extracting the header-extraction and request-parsing into a helper (e.g. `parseIncomingRequest(format, request)`) to reduce the cognitive load of the main handler. The current structure is functional but approaches the point where a new engineer would need to read the entire function to understand any part of it.",
175
+ "confidence": "low"
176
+ },
177
+ {
178
+ "dimension": "ai_generated_debt",
179
+ "identifier": "type_files_high_comment_ratio",
180
+ "summary": "Type definition files have disproportionately high comment ratios (31-32%) relative to codebase average (4.2%)",
181
+ "related_files": [
182
+ "src/types/request.ts",
183
+ "src/types/rule.ts"
184
+ ],
185
+ "evidence": [
186
+ "src/types/request.ts has 32% comment ratio vs 4.2% codebase average. src/types/rule.ts has 31% comment ratio. Many comments restate what the type signature already communicates. For example, in request.ts: `readonly lastMessage: string` has comment `/** The last user message's text. This is what most matchers check. */` -- the second sentence adds value but the first sentence restates the name. In rule.ts: `/** Returned by `when()`. Call `.reply()` or `.replySequence()` on it to complete the rule. */` on PendingRule -- the interface definition below makes this obvious."
187
+ ],
188
+ "suggestion": "Trim JSDoc comments on types to only include non-obvious information. Keep comments that explain 'why' or usage guidance (like 'This is what most matchers check'), but remove comments that restate the type name or signature. For example, `readonly tools?: readonly ToolDef[] | undefined;` needs no doc comment; the name and type are self-documenting.",
189
+ "confidence": "medium"
190
+ },
191
+ {
192
+ "dimension": "incomplete_migration",
193
+ "identifier": "single_require_in_esm_codebase",
194
+ "summary": "cli.ts uses createRequire to read package.json version, the only require() in an otherwise pure ESM codebase",
195
+ "related_files": [
196
+ "src/cli.ts"
197
+ ],
198
+ "evidence": [
199
+ "Line 4: `import { createRequire } from 'node:module'` and line 17: `const require = createRequire(import.meta.url); const { version } = require('../package.json') as { version: string };`. The entire codebase is ESM (type: module in package.json, all other imports use ESM syntax). This is the sole use of require()."
200
+ ],
201
+ "suggestion": "Use an import assertion/attribute to load the package.json: `import pkg from '../package.json' with { type: 'json' };` (Node 22+ supports this). This eliminates the require shim and makes the codebase fully ESM.",
202
+ "confidence": "medium"
203
+ },
204
+ {
205
+ "dimension": "cross_module_architecture",
206
+ "identifier": "dynamic_import_of_loader_in_server",
207
+ "summary": "MockServer.load() uses a dynamic import for loader.js, creating a hidden runtime dependency that static analysis cannot trace",
208
+ "related_files": [
209
+ "src/mock-server.ts",
210
+ "src/loader.ts"
211
+ ],
212
+ "evidence": [
213
+ "Line 187: `const { loadRulesFromPath } = await import('./loader.js');`. This dynamic import means loader.ts is not in the static dependency graph of mock-server.ts. While this may be intentional for tree-shaking (users who never call load() don't pay for the loader code), it creates an invisible architecture edge that tools and developers cannot see without reading the implementation."
214
+ ],
215
+ "suggestion": "Add a comment explaining the intent (tree-shaking/code-splitting). If tree-shaking is not a goal, convert to a static import. If it is intentional, document this in the module's JSDoc: `/** Dynamically imports loader.js to keep it out of the bundle for users who don't use file-based rules. */`",
216
+ "confidence": "medium"
217
+ },
218
+ {
219
+ "dimension": "mid_level_elegance",
220
+ "identifier": "cli_watch_debounce_races",
221
+ "summary": "CLI watch mode uses a boolean flag for debouncing that can miss rapid successive changes",
222
+ "related_files": [
223
+ "src/cli.ts"
224
+ ],
225
+ "evidence": [
226
+ "Lines 88-103: The watch handler uses a `reloading` boolean flag and setTimeout with WATCH_DEBOUNCE_MS=100. If a change fires while reloading is true (during the 100ms timeout or during the async reload), it is silently dropped. A rapid sequence of saves could result in the first change being loaded and the last (most current) being missed entirely, leaving stale rules loaded."
227
+ ],
228
+ "suggestion": "Use a proper debounce pattern that resets the timer on each new event, ensuring the final change is always processed: store the timeout ID and clear/reset it on each watch callback. Example: `let timer: NodeJS.Timeout | undefined; watch(..., () => { clearTimeout(timer); timer = setTimeout(async () => { ... }, WATCH_DEBOUNCE_MS); });`",
229
+ "confidence": "high"
230
+ },
231
+ {
232
+ "dimension": "high_level_elegance",
233
+ "identifier": "format_interface_serializeError_status_unused",
234
+ "summary": "Format.serializeError receives status but all three implementations ignore it in the response body",
235
+ "related_files": [
236
+ "src/formats/types.ts",
237
+ "src/formats/openai/serialize.ts",
238
+ "src/formats/anthropic/serialize.ts",
239
+ "src/formats/responses/serialize.ts"
240
+ ],
241
+ "evidence": [
242
+ "The Format interface (types.ts line 25) defines serializeError with `status: number` in the parameter. But all three implementations ignore the status field when constructing the error body -- OpenAI (serialize.ts line 134-146) only uses message and type, Anthropic (serialize.ts line 170-179) only uses message and type, Responses (serialize.ts line 315-328) only uses message and type. The status is used by route-handler.ts (line 113) to set the HTTP status code, not by the serializer. The parameter creates a false expectation that the serializer should use it."
243
+ ],
244
+ "suggestion": "Remove `status` from the serializeError parameter signature in the Format interface and all implementations, since it is only used by the route handler for the HTTP status code, not by the serialization logic. This makes the boundary clearer.",
245
+ "confidence": "medium"
246
+ },
247
+ {
248
+ "dimension": "abstraction_fitness",
249
+ "identifier": "buildMockRequest_seven_params",
250
+ "summary": "buildMockRequest takes 7 positional parameters, making call sites hard to read",
251
+ "related_files": [
252
+ "src/formats/request-helpers.ts"
253
+ ],
254
+ "evidence": [
255
+ "buildMockRequest (line 25-51) accepts: format, body, messages, tools, defaultModel, raw, meta. All three callers (openai/parse.ts:35, anthropic/parse.ts:58, responses/parse.ts:61) pass 7 positional arguments. The call sites read like `buildMockRequest('openai', req, parseMessages(req), parseTools(req), 'gpt-5.4', body, meta)` which requires counting positions to understand what each argument is."
256
+ ],
257
+ "suggestion": "Group the parameters into an options object: `buildMockRequest({ format: 'openai', body: req, messages: parseMessages(req), tools: parseTools(req), defaultModel: 'gpt-5.4', raw: body, meta })`. This is a common pattern for functions with more than 4-5 parameters and makes call sites self-documenting.",
258
+ "confidence": "medium"
259
+ },
260
+ {
261
+ "dimension": "package_organization",
262
+ "identifier": "test_helpers_not_in_dedicated_dir",
263
+ "summary": "Test helper (make-req.ts) is in test/helpers/ but format-specific test helpers are inline in each test file",
264
+ "related_files": [
265
+ "test/helpers/make-req.ts",
266
+ "test/formats/openai.test.ts",
267
+ "test/formats/anthropic.test.ts"
268
+ ],
269
+ "evidence": [
270
+ "test/helpers/make-req.ts is a shared fixture factory extracted to its own directory",
271
+ "Format test files each define their own inline parse() helper rather than sharing one",
272
+ "Minor inconsistency in where test utilities live"
273
+ ],
274
+ "suggestion": "Consider extracting common test parsing helpers alongside make-req.ts in test/helpers/ for consistency.",
275
+ "confidence": "low"
276
+ }
277
+ ],
278
+ "dimension_notes": {
279
+ "authorization_consistency": "This is a mock server intended for local testing. Routes are intentionally unauthenticated by design -- the server's purpose is to simulate LLM API responses in test environments. No auth gaps exist because auth is not part of the domain.",
280
+ "cross_module_architecture": "The codebase has clean dependency direction: types flow outward, formats depend on shared helpers, mock-server orchestrates. The one notable edge is the dynamic import of loader.js, which is reported as a finding. Otherwise, boundaries are well-defined.",
281
+ "convention_outlier": "The three format modules (openai, anthropic, responses) follow an identical file structure (index.ts, parse.ts, schema.ts, serialize.ts) which is excellent consistency. Minor outlier is the duplicated buildUsage function naming across serializers.",
282
+ "incomplete_migration": "Nearly fully ESM. The single createRequire usage in cli.ts is the only CJS residue.",
283
+ "package_organization": "For a project of this size (~40 files), the directory layout is well-organized with clear domain boundaries. The formats/ subdirectory with per-provider modules is clean.",
284
+ "high_level_elegance": "The decomposition is clear: types define contracts, formats handle protocol translation, rule-engine handles matching, mock-server orchestrates. The Format interface provides a clean plugin boundary. Minor concern about serializeError parameter mismatch is reported.",
285
+ "abstraction_fitness": "Abstractions are generally well-fitted. The Format interface earns its keep across three implementations. The buildMockRequest parameter count is the main concern.",
286
+ "api_surface_coherence": "The public API (MockServer class) is coherent. The mixed sync/async in cli-validators is the notable inconsistency.",
287
+ "mid_level_elegance": "Handoffs between modules are mostly clean. The sequence resolver mutation and the CLI watch debounce pattern are the notable rough edges.",
288
+ "low_level_elegance": "Implementation craft is generally good. The ID generation collision risk and the hardcoded tool index in serializeComplete are the main issues.",
289
+ "design_coherence": "Functions are mostly focused. The duplicated sequence-rule logic between MockServer.when() and loader.ts is the clearest design coherence issue.",
290
+ "ai_generated_debt": "The codebase is generally clean of AI-generated patterns. The type files have somewhat elevated comment ratios with some restating comments, but the comments do generally add value.",
291
+ "error_consistency": "Error handling is functional but inconsistent in a few places: resolver errors are silently swallowed with fallback, unknown file extensions are silently skipped, and history recording timing is inconsistent between error and success paths."
292
+ },
293
+ "provenance": {
294
+ "kind": "blind_review_batch_import",
295
+ "blind": true,
296
+ "runner": "claude",
297
+ "run_stamp": "ext_20260315_045546_0587ea3b",
298
+ "created_at": "2026-03-15T05:00:28+00:00",
299
+ "packet_path": "/Users/suyash.x.srijan/Documents/Personal_Projects/llm-mock-server/.desloppify/review_packet_blind.json",
300
+ "packet_sha256": "1405a7ac30145db0e952bfb0b7abd92e594e863c7dd65c60dadbfe28680cf423",
301
+ "external_session_id": "ext_20260315_045546_0587ea3b"
302
+ }
303
+ }
@@ -0,0 +1,17 @@
1
+ # Claude Blind Reviewer Launch Prompt
2
+
3
+ You are an isolated blind reviewer. Do not use prior chat context, prior score history, or target-score anchoring.
4
+
5
+ Blind packet: /Users/suyash.x.srijan/Documents/Personal_Projects/llm-mock-server/.desloppify/review_packet_blind.json
6
+ Template JSON: /Users/suyash.x.srijan/Documents/Personal_Projects/llm-mock-server/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json
7
+ Output JSON path: /Users/suyash.x.srijan/Documents/Personal_Projects/llm-mock-server/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json
8
+
9
+ Requirements:
10
+ 1. Read ONLY the blind packet and repository code.
11
+ 2. Start from the template JSON so `session.id` and `session.token` are preserved.
12
+ 3. Keep `session.id` exactly `ext_20260315_045546_0587ea3b`.
13
+ 4. Keep `session.token` exactly `ec214bb085366c91ba9d2310e32c5e2d`.
14
+ 5. Output must be valid JSON with top-level keys: session, assessments, findings.
15
+ 6. Every finding must include: dimension, identifier, summary, related_files, evidence, suggestion, confidence.
16
+ 7. Do not include provenance metadata (CLI injects canonical provenance).
17
+ 8. Return JSON only (no markdown fences).