prism-mcp-server 20.2.6 → 20.2.7

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.
@@ -30,21 +30,24 @@
30
30
  */
31
31
  import { BRAVE_API_KEY, BRAVE_ANSWERS_API_KEY } from "../config.js";
32
32
  import { SYNALUX_SEARCH_AVAILABLE, synaluxWebSearch, synaluxWebSearchRaw, synaluxLocalSearch, synaluxLocalSearchRaw, synaluxBraveAnswers, } from "./synaluxSearch.js";
33
- import { debugLog } from "./logger.js";
33
+ const BRAVE_API_KEY_MISSING_ERROR = "BRAVE_API_KEY is not configured";
34
+ const BRAVE_ANSWERS_API_KEY_MISSING_ERROR = "BRAVE_ANSWERS_API_KEY is not configured";
35
+ const SYNALUX_OFFSET_UNSUPPORTED_ERROR = "Configured Synalux search does not support search offsets";
36
+ function requireBraveApiKey() {
37
+ if (!BRAVE_API_KEY)
38
+ throw new Error(BRAVE_API_KEY_MISSING_ERROR);
39
+ return BRAVE_API_KEY;
40
+ }
34
41
  // Brave Answers API call (AI Grounding/OpenAI-compatible)
35
42
  export async function performBraveAnswers(query, model = "brave") {
36
- // Route through Synalux portal when available
43
+ // A configured Synalux account is a privacy boundary: provider
44
+ // credentials and redaction stay portal-side. Never escape to a direct
45
+ // provider with the original query when that portal request fails.
37
46
  if (SYNALUX_SEARCH_AVAILABLE) {
38
- try {
39
- return await synaluxBraveAnswers(query, model);
40
- }
41
- catch (err) {
42
- debugLog(`[braveApi] Synalux answers failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
43
- // Fall through to direct Brave API
44
- }
47
+ return synaluxBraveAnswers(query, model);
45
48
  }
46
49
  if (!BRAVE_ANSWERS_API_KEY) {
47
- throw new Error("BRAVE_ANSWERS_API_KEY is not configured");
50
+ throw new Error(BRAVE_ANSWERS_API_KEY_MISSING_ERROR);
48
51
  }
49
52
  const url = new URL("https://api.search.brave.com/res/v1/chat/completions");
50
53
  const messages = [{ role: "user", content: query }];
@@ -74,16 +77,12 @@ export async function performBraveAnswers(query, model = "brave") {
74
77
  }
75
78
  // Raw web search API call
76
79
  export async function performWebSearchRaw(query, count = 10, offset = 0) {
77
- // Route through Synalux portal when available (offset=0 only — portal doesn't support offset)
78
- if (SYNALUX_SEARCH_AVAILABLE && offset === 0) {
79
- try {
80
- return await synaluxWebSearchRaw(query, count);
81
- }
82
- catch (err) {
83
- debugLog(`[braveApi] Synalux search failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
84
- // Fall through to direct Brave API
85
- }
80
+ if (SYNALUX_SEARCH_AVAILABLE) {
81
+ if (offset !== 0)
82
+ throw new Error(SYNALUX_OFFSET_UNSUPPORTED_ERROR);
83
+ return synaluxWebSearchRaw(query, count);
86
84
  }
85
+ const braveApiKey = requireBraveApiKey();
87
86
  const url = new URL("https://api.search.brave.com/res/v1/web/search");
88
87
  url.searchParams.set("q", query);
89
88
  url.searchParams.set("count", Math.min(count, 20).toString()); // API limit
@@ -92,7 +91,7 @@ export async function performWebSearchRaw(query, count = 10, offset = 0) {
92
91
  headers: {
93
92
  Accept: "application/json",
94
93
  "Accept-Encoding": "gzip",
95
- "X-Subscription-Token": BRAVE_API_KEY,
94
+ "X-Subscription-Token": braveApiKey,
96
95
  },
97
96
  signal: AbortSignal.timeout(15_000),
98
97
  });
@@ -103,15 +102,10 @@ export async function performWebSearchRaw(query, count = 10, offset = 0) {
103
102
  }
104
103
  // Web search API call
105
104
  export async function performWebSearch(query, count = 10, offset = 0) {
106
- // Route through Synalux portal when available (offset=0 only — portal doesn't support offset)
107
- if (SYNALUX_SEARCH_AVAILABLE && offset === 0) {
108
- try {
109
- return await synaluxWebSearch(query, count);
110
- }
111
- catch (err) {
112
- debugLog(`[braveApi] Synalux search failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
113
- // Fall through to direct Brave API
114
- }
105
+ if (SYNALUX_SEARCH_AVAILABLE) {
106
+ if (offset !== 0)
107
+ throw new Error(SYNALUX_OFFSET_UNSUPPORTED_ERROR);
108
+ return synaluxWebSearch(query, count);
115
109
  }
116
110
  const textData = await performWebSearchRaw(query, count, offset);
117
111
  const data = JSON.parse(textData);
@@ -127,13 +121,14 @@ export async function performWebSearch(query, count = 10, offset = 0) {
127
121
  }
128
122
  // Get POI details
129
123
  export async function getPoisData(ids) {
124
+ const braveApiKey = requireBraveApiKey();
130
125
  const url = new URL("https://api.search.brave.com/res/v1/local/pois");
131
126
  ids.filter(Boolean).forEach((id) => url.searchParams.append("ids", id));
132
127
  const response = await fetch(url, {
133
128
  headers: {
134
129
  Accept: "application/json",
135
130
  "Accept-Encoding": "gzip",
136
- "X-Subscription-Token": BRAVE_API_KEY,
131
+ "X-Subscription-Token": braveApiKey,
137
132
  },
138
133
  signal: AbortSignal.timeout(15_000),
139
134
  });
@@ -144,13 +139,14 @@ export async function getPoisData(ids) {
144
139
  }
145
140
  // Get descriptions data
146
141
  export async function getDescriptionsData(ids) {
142
+ const braveApiKey = requireBraveApiKey();
147
143
  const url = new URL("https://api.search.brave.com/res/v1/local/descriptions");
148
144
  ids.filter(Boolean).forEach((id) => url.searchParams.append("ids", id));
149
145
  const response = await fetch(url, {
150
146
  headers: {
151
147
  Accept: "application/json",
152
148
  "Accept-Encoding": "gzip",
153
- "X-Subscription-Token": BRAVE_API_KEY,
149
+ "X-Subscription-Token": braveApiKey,
154
150
  },
155
151
  signal: AbortSignal.timeout(15_000),
156
152
  });
@@ -168,16 +164,10 @@ function chunkArray(arr, size) {
168
164
  }
169
165
  // Raw local search API call with poi/details payload
170
166
  export async function performLocalSearchRaw(query, count = 5) {
171
- // Route through Synalux portal when available
172
167
  if (SYNALUX_SEARCH_AVAILABLE) {
173
- try {
174
- return await synaluxLocalSearchRaw(query, count);
175
- }
176
- catch (err) {
177
- debugLog(`[braveApi] Synalux local search raw failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
178
- // Fall through to direct Brave API
179
- }
168
+ return synaluxLocalSearchRaw(query, count);
180
169
  }
170
+ const braveApiKey = requireBraveApiKey();
181
171
  // Initial search to get location IDs
182
172
  const webUrl = new URL("https://api.search.brave.com/res/v1/web/search");
183
173
  webUrl.searchParams.set("q", query);
@@ -188,7 +178,7 @@ export async function performLocalSearchRaw(query, count = 5) {
188
178
  headers: {
189
179
  Accept: "application/json",
190
180
  "Accept-Encoding": "gzip",
191
- "X-Subscription-Token": BRAVE_API_KEY,
181
+ "X-Subscription-Token": braveApiKey,
192
182
  },
193
183
  signal: AbortSignal.timeout(15_000),
194
184
  });
@@ -232,15 +222,8 @@ export async function performLocalSearchRaw(query, count = 5) {
232
222
  }
233
223
  // Local search API call with poi details
234
224
  export async function performLocalSearch(query, count = 5) {
235
- // Route through Synalux portal when available
236
225
  if (SYNALUX_SEARCH_AVAILABLE) {
237
- try {
238
- return await synaluxLocalSearch(query, count);
239
- }
240
- catch (err) {
241
- debugLog(`[braveApi] Synalux local search failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
242
- // Fall through to direct Brave API
243
- }
226
+ return synaluxLocalSearch(query, count);
244
227
  }
245
228
  const rawData = await performLocalSearchRaw(query, count);
246
229
  const parsed = JSON.parse(rawData);
@@ -0,0 +1,400 @@
1
+ import { spawnSync } from "node:child_process";
2
+ const IMPLEMENTATION_REQUEST_RE = /\b(?:implement|write|create|generate|complete|finish|fix)\b[\s\S]{0,160}\b(?:code|source|function|method|class|interface|struct|enum|implementation|algorithm|component|endpoint)\b/i;
3
+ const STRICT_SOURCE_REQUEST_RE = /\b(?:return|output|respond with)\s+only\s+(?:the\s+)?(?:implementation\s+)?(?:source\s+)?code\b/i;
4
+ const CODE_SHAPE_RE = /(?:^|\n)\s*(?:(?:export|public|private|protected|internal|open|pub|static|final|abstract|async)\s+)*(?:class|interface|struct|enum|function|def|func|fun|fn|type)\s+[A-Za-z_$][\w$]*|(?:^|\n)\s*(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=|(?:^|\n)\s*(?:[A-Za-z_$][\w$:<>,.?*[\]&]*\s+)+[A-Za-z_$][\w$]*\s*\([^;\n]*\)\s*(?:const\s*)?(?:noexcept\s*)?(?:\{|=>)|=>\s*[{(]/m;
5
+ export const INCOMPLETE_IMPLEMENTATION_PATTERNS = [
6
+ {
7
+ reason: "code_placeholder",
8
+ pattern: /\b(?:your code here|implementation goes here|insert implementation here)\b/i,
9
+ },
10
+ {
11
+ reason: "code_placeholder",
12
+ pattern: /\b(?:implementation|code)\s+(?:is\s+)?(?:omitted|left out|not shown)\b/i,
13
+ },
14
+ {
15
+ reason: "code_placeholder",
16
+ pattern: /\b(?:rest|remainder)\s+of\s+(?:the\s+)?(?:implementation|code)\b/i,
17
+ },
18
+ {
19
+ reason: "code_placeholder",
20
+ pattern: /(?:#|\/\/|\/\*+|\*)\s*(?:TODO|FIXME)\s*:?\s*(?:implement|complete|finish|add)\b/i,
21
+ },
22
+ {
23
+ reason: "code_not_implemented",
24
+ pattern: /\braise\s+NotImplementedError\b/,
25
+ },
26
+ {
27
+ reason: "code_unfinished_reasoning",
28
+ pattern: /(?:^|\n)\s*(?:#|\/\/|\/\*+|\*)?\s*(?:actually,?\s+)?(?:let me think|i need to (?:think|finish|complete)|to be continued)\b/i,
29
+ },
30
+ ];
31
+ const REQUIRED_SYMBOL_PATTERNS = [
32
+ /\b(?:implement|write|create|generate|complete)\s+(?:an?\s+)?(?:class|function|method|interface|struct|enum)\s+[`'"]?([A-Za-z_$][\w$]*)/gi,
33
+ /\bimplement\s+[`'"]?([A-Za-z_$][\w$]*)\s*\(/gi,
34
+ ];
35
+ const PYTHON_CLASS_RE = /^(\s*)class\s+([A-Za-z_]\w*)\s*(?:\([^)]*\))?\s*:/;
36
+ const PYTHON_FUNCTION_RE = /^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(([^)]*)\)/;
37
+ const PYTHON_DIRECT_PRIVATE_CALL_RE = /\bself\.(_[A-Za-z_]\w*)\s*\(/g;
38
+ const PYTHON_METHOD_DECORATOR_RE = /^(\s*)@(staticmethod|classmethod)\b/;
39
+ const PYTHON_SIGNAL_RE = /```(?:python|py)\b|(?:^|\n)\s*(?:from\s+\S+\s+import|import\s+\S+|(?:async\s+)?def\s+|class\s+\w+.*:)/im;
40
+ const UNFENCED_PYTHON_START_RE = /^\s*(?:from\s+\S+\s+import|import\s+\S+|(?:async\s+)?def\s+|class\s+\w+)/i;
41
+ const TRAILING_PROSE_LINE_RE = /^(?!(?:async\s+def|def|class|from|import|return|raise|yield|assert|del|global|nonlocal|if|elif|else|for|while|try|except|finally|with|match|case)\b)(?![A-Za-z_]\w*\s*=)[A-Za-z][A-Za-z'’-]*:?(?:\s+(?:[A-Za-z][A-Za-z'’+/-]*|`[^`\r\n]+`|https?:\/\/\S+|\d[\w.,%()+\-]*|[+*=<>-])){2,}[.!?]$/;
42
+ const TRAILING_URL_LINE_RE = /^https?:\/\/\S+$/i;
43
+ const PYTHON_INVALID_DEF_RE = /(?:^|\n)\s*(?:async\s+)?def\s+[A-Za-z_]\w*\s*:/m;
44
+ const PYTHON_AST_SCRIPT = "import ast,sys; tree=ast.parse(sys.stdin.read()); compile(tree, '<prism-coding-gate>', 'exec')";
45
+ const PYTHON_COMMANDS = ["python3", "python"];
46
+ const PYTHON_CHILDREN_KEYS_UNPACK_RE = /\bfor\s+[A-Za-z_]\w*\s*,\s*child(?:_node)?\s+in\s+(?:sorted\(\s*)?[A-Za-z_][\w.]*\.children\.keys\(\)\s*\)?\s*:/;
47
+ function extractUnfencedPythonCode(output) {
48
+ const lines = output.trim().split(/\r?\n/);
49
+ const start = lines.findIndex((line) => UNFENCED_PYTHON_START_RE.test(line));
50
+ if (start < 0)
51
+ return undefined;
52
+ let end = lines.length;
53
+ for (let index = start + 1; index < lines.length; index++) {
54
+ const line = lines[index];
55
+ const previousLine = lines[index - 1];
56
+ if (previousLine.trim().length === 0 &&
57
+ indentation(line) === 0 &&
58
+ (TRAILING_PROSE_LINE_RE.test(line.trim()) ||
59
+ TRAILING_URL_LINE_RE.test(line.trim()))) {
60
+ end = index;
61
+ break;
62
+ }
63
+ }
64
+ const code = lines.slice(start, end).join("\n").trim();
65
+ return code || undefined;
66
+ }
67
+ function extractCode(output) {
68
+ const blocks = [
69
+ ...output.matchAll(/```([A-Za-z0-9_+#.-]+)?\s*\n([\s\S]*?)```/g),
70
+ ].map((match) => ({
71
+ language: match[1]?.toLowerCase(),
72
+ code: match[2].trim(),
73
+ })).filter((block) => block.code.length > 0);
74
+ if (blocks.length === 0) {
75
+ const python = extractUnfencedPythonCode(output);
76
+ return {
77
+ all: output.trim(),
78
+ ...(python ? { python } : {}),
79
+ hasFences: false,
80
+ };
81
+ }
82
+ const pythonBlocks = blocks
83
+ .filter((block) => (block.language === "python" ||
84
+ block.language === "py" ||
85
+ (!block.language && PYTHON_SIGNAL_RE.test(block.code))))
86
+ .map((block) => block.code);
87
+ return {
88
+ all: blocks.map((block) => block.code).join("\n\n"),
89
+ ...(pythonBlocks.length > 0
90
+ ? { python: pythonBlocks.join("\n\n") }
91
+ : {}),
92
+ hasFences: true,
93
+ };
94
+ }
95
+ function indentation(line) {
96
+ return line.match(/^\s*/)?.[0].replace(/\t/g, " ").length ?? 0;
97
+ }
98
+ function pythonBlockEnd(lines, start, blockIndent) {
99
+ let end = start + 1;
100
+ while (end < lines.length) {
101
+ const line = lines[end];
102
+ if (line.trim() && indentation(line) <= blockIndent)
103
+ break;
104
+ end++;
105
+ }
106
+ return end;
107
+ }
108
+ function directBodyIndent(lines, start, end) {
109
+ const indents = lines
110
+ .slice(start + 1, end)
111
+ .filter((line) => line.trim() && !line.trimStart().startsWith("#"))
112
+ .map(indentation);
113
+ return indents.length > 0 ? Math.min(...indents) : undefined;
114
+ }
115
+ function findConstructorMissingReceiverAssignments(lines) {
116
+ const assignments = [];
117
+ for (let classIndex = 0; classIndex < lines.length; classIndex++) {
118
+ const classMatch = lines[classIndex].match(PYTHON_CLASS_RE);
119
+ if (!classMatch)
120
+ continue;
121
+ const classIndent = indentation(lines[classIndex]);
122
+ const classEnd = pythonBlockEnd(lines, classIndex, classIndent);
123
+ const classBodyIndent = directBodyIndent(lines, classIndex, classEnd);
124
+ if (classBodyIndent === undefined)
125
+ continue;
126
+ const selfAttributes = new Set();
127
+ const selfAttributeRe = /\bself\.([A-Za-z_]\w*)\b/g;
128
+ for (const line of lines.slice(classIndex + 1, classEnd)) {
129
+ for (const match of line.matchAll(selfAttributeRe)) {
130
+ selfAttributes.add(match[1]);
131
+ }
132
+ }
133
+ for (let functionIndex = classIndex + 1; functionIndex < classEnd; functionIndex++) {
134
+ const functionMatch = lines[functionIndex].match(PYTHON_FUNCTION_RE);
135
+ if (!functionMatch ||
136
+ functionMatch[2] !== "__init__" ||
137
+ indentation(lines[functionIndex]) !== classBodyIndent) {
138
+ continue;
139
+ }
140
+ const functionIndent = indentation(lines[functionIndex]);
141
+ const functionEnd = pythonBlockEnd(lines, functionIndex, functionIndent);
142
+ const functionBodyIndent = directBodyIndent(lines, functionIndex, functionEnd);
143
+ if (functionBodyIndent === undefined)
144
+ continue;
145
+ const functionBody = lines
146
+ .slice(functionIndex + 1, functionEnd)
147
+ .join("\n");
148
+ for (let bodyIndex = functionIndex + 1; bodyIndex < functionEnd; bodyIndex++) {
149
+ if (indentation(lines[bodyIndex]) !== functionBodyIndent)
150
+ continue;
151
+ const bareAssignment = lines[bodyIndex].match(/^\s*([A-Za-z_]\w*)\s*=(?!=)/);
152
+ if (!bareAssignment || !selfAttributes.has(bareAssignment[1]))
153
+ continue;
154
+ const bareUseRe = new RegExp(`(?<!\\.)\\b${bareAssignment[1]}\\b`, "g");
155
+ if ([...functionBody.matchAll(bareUseRe)].length !== 1)
156
+ continue;
157
+ assignments.push({
158
+ lineIndex: bodyIndex,
159
+ name: bareAssignment[1],
160
+ });
161
+ }
162
+ }
163
+ classIndex = classEnd - 1;
164
+ }
165
+ return assignments;
166
+ }
167
+ function requiredSymbols(prompt) {
168
+ const symbols = new Set();
169
+ for (const pattern of REQUIRED_SYMBOL_PATTERNS) {
170
+ pattern.lastIndex = 0;
171
+ for (const match of prompt.matchAll(pattern)) {
172
+ if (match[1])
173
+ symbols.add(match[1]);
174
+ }
175
+ }
176
+ return [...symbols];
177
+ }
178
+ function pythonStructureFailure(code) {
179
+ if (!/(?:^|\n)\s*class\s+[A-Za-z_]\w*[\s(:]/m.test(code) ||
180
+ !/(?:^|\n)\s*(?:async\s+)?def\s+/m.test(code)) {
181
+ return undefined;
182
+ }
183
+ const lines = code.split(/\r?\n/);
184
+ const scopes = [];
185
+ const classScopes = [];
186
+ let pendingDecorator;
187
+ for (const line of lines) {
188
+ if (!line.trim() || line.trimStart().startsWith("#"))
189
+ continue;
190
+ const indent = indentation(line);
191
+ while (scopes.length > 0 && scopes[scopes.length - 1].indent >= indent) {
192
+ scopes.pop();
193
+ }
194
+ const decorator = line.match(PYTHON_METHOD_DECORATOR_RE);
195
+ if (decorator) {
196
+ pendingDecorator = {
197
+ indent,
198
+ kind: decorator[2],
199
+ };
200
+ continue;
201
+ }
202
+ const classMatch = line.match(PYTHON_CLASS_RE);
203
+ if (classMatch) {
204
+ const baseList = line.match(/\(([^)]*)\)/)?.[1].trim() ?? "";
205
+ const classScope = {
206
+ hasBase: baseList.length > 0,
207
+ definedMethods: new Set(),
208
+ directPrivateCalls: new Set(),
209
+ supportsDynamicLookup: false,
210
+ };
211
+ classScopes.push(classScope);
212
+ scopes.push({ kind: "class", indent, classScope });
213
+ pendingDecorator = undefined;
214
+ continue;
215
+ }
216
+ const functionMatch = line.match(PYTHON_FUNCTION_RE);
217
+ if (functionMatch) {
218
+ const parent = scopes[scopes.length - 1];
219
+ const methodName = functionMatch[2];
220
+ const parameters = functionMatch[3]
221
+ .split(",")
222
+ .map((parameter) => parameter.trim())
223
+ .filter(Boolean);
224
+ if (parent?.kind === "class") {
225
+ parent.classScope?.definedMethods.add(methodName);
226
+ if (methodName === "__getattr__" || methodName === "__getattribute__") {
227
+ if (parent.classScope)
228
+ parent.classScope.supportsDynamicLookup = true;
229
+ }
230
+ const decoratedStatic = pendingDecorator?.indent === indent &&
231
+ pendingDecorator.kind === "staticmethod";
232
+ const expectedReceiver = pendingDecorator?.indent === indent &&
233
+ pendingDecorator.kind === "classmethod"
234
+ ? "cls"
235
+ : "self";
236
+ if (!decoratedStatic && parameters[0]?.split(/[:=]/, 1)[0].trim() !== expectedReceiver) {
237
+ return "python_method_missing_receiver";
238
+ }
239
+ }
240
+ scopes.push({ kind: "function", indent });
241
+ pendingDecorator = undefined;
242
+ }
243
+ else {
244
+ pendingDecorator = undefined;
245
+ }
246
+ const containingClass = [...scopes]
247
+ .reverse()
248
+ .find((scope) => scope.kind === "class")
249
+ ?.classScope;
250
+ PYTHON_DIRECT_PRIVATE_CALL_RE.lastIndex = 0;
251
+ for (const match of line.matchAll(PYTHON_DIRECT_PRIVATE_CALL_RE)) {
252
+ containingClass?.directPrivateCalls.add(match[1]);
253
+ }
254
+ }
255
+ for (const classScope of classScopes) {
256
+ if (classScope.hasBase || classScope.supportsDynamicLookup)
257
+ continue;
258
+ for (const called of classScope.directPrivateCalls) {
259
+ if (!classScope.definedMethods.has(called)) {
260
+ return "python_undefined_private_helper";
261
+ }
262
+ }
263
+ }
264
+ return undefined;
265
+ }
266
+ function pythonSyntaxFailure(code) {
267
+ if (!PYTHON_SIGNAL_RE.test(code))
268
+ return undefined;
269
+ if (PYTHON_INVALID_DEF_RE.test(code))
270
+ return "python_syntax_error";
271
+ for (const command of PYTHON_COMMANDS) {
272
+ const parsed = spawnSync(command, ["-c", PYTHON_AST_SCRIPT], {
273
+ input: code,
274
+ encoding: "utf8",
275
+ timeout: 2_000,
276
+ maxBuffer: 64 * 1024,
277
+ windowsHide: true,
278
+ });
279
+ if (parsed.error && parsed.error.code === "ENOENT") {
280
+ continue;
281
+ }
282
+ return parsed.status === 0 ? undefined : "python_syntax_error";
283
+ }
284
+ return undefined;
285
+ }
286
+ function pythonStaticContractFailure(code) {
287
+ const issues = new Set();
288
+ const lines = code.split(/\r?\n/);
289
+ if (findConstructorMissingReceiverAssignments(lines).length > 0) {
290
+ issues.add("constructor_attribute_missing_receiver");
291
+ }
292
+ if (PYTHON_CHILDREN_KEYS_UNPACK_RE.test(code)) {
293
+ issues.add("dict_keys_unpack");
294
+ }
295
+ return issues.size > 0
296
+ ? `python_static_contract:${[...issues].sort().join(",")}`
297
+ : undefined;
298
+ }
299
+ export function applyDeterministicCodingRepairs(output, reason) {
300
+ if (!reason.startsWith("python_static_contract:")) {
301
+ return { output, changes: [] };
302
+ }
303
+ const lines = output.split(/\r?\n/);
304
+ const changes = new Set();
305
+ if (reason.includes("constructor_attribute_missing_receiver")) {
306
+ for (const { lineIndex, name } of findConstructorMissingReceiverAssignments(lines)) {
307
+ const bareAssignment = lines[lineIndex].match(/^(\s*)([A-Za-z_]\w*)(\s*=(?!=).*)$/);
308
+ if (!bareAssignment || bareAssignment[2] !== name)
309
+ continue;
310
+ lines[lineIndex] =
311
+ `${bareAssignment[1]}self.${name}${bareAssignment[3]}`;
312
+ changes.add("constructor_attribute_missing_receiver");
313
+ }
314
+ }
315
+ if (reason.includes("dict_keys_unpack")) {
316
+ for (let index = 0; index < lines.length; index++) {
317
+ if (!PYTHON_CHILDREN_KEYS_UNPACK_RE.test(lines[index]))
318
+ continue;
319
+ lines[index] = lines[index].replace(".keys()", ".items()");
320
+ changes.add("dict_keys_unpack");
321
+ }
322
+ }
323
+ return {
324
+ output: lines.join(output.includes("\r\n") ? "\r\n" : "\n"),
325
+ changes: [...changes].sort(),
326
+ };
327
+ }
328
+ /**
329
+ * Reject only objective implementation failures. Non-implementation analysis
330
+ * in code mode remains valid prose and bypasses this specialized gate.
331
+ */
332
+ export function passesCodingQualityGate(prompt, output) {
333
+ const implementationRequested = IMPLEMENTATION_REQUEST_RE.test(prompt) || STRICT_SOURCE_REQUEST_RE.test(prompt);
334
+ if (!implementationRequested)
335
+ return { pass: true };
336
+ for (const { reason, pattern } of INCOMPLETE_IMPLEMENTATION_PATTERNS) {
337
+ if (pattern.test(output))
338
+ return { pass: false, reason };
339
+ }
340
+ const extracted = extractCode(output);
341
+ const code = extracted.all;
342
+ if (STRICT_SOURCE_REQUEST_RE.test(prompt) && !CODE_SHAPE_RE.test(code)) {
343
+ return { pass: false, reason: "code_shape_missing" };
344
+ }
345
+ for (const symbol of requiredSymbols(prompt)) {
346
+ const symbolRe = new RegExp(`\\b${symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
347
+ if (!symbolRe.test(code)) {
348
+ return { pass: false, reason: "code_required_symbol_missing" };
349
+ }
350
+ }
351
+ const pythonCode = extracted.python;
352
+ if (pythonCode) {
353
+ const pythonSyntax = pythonSyntaxFailure(pythonCode);
354
+ if (pythonSyntax)
355
+ return { pass: false, reason: pythonSyntax };
356
+ const pythonStaticContract = pythonStaticContractFailure(pythonCode);
357
+ if (pythonStaticContract) {
358
+ return { pass: false, reason: pythonStaticContract };
359
+ }
360
+ const pythonFailure = pythonStructureFailure(pythonCode);
361
+ if (pythonFailure)
362
+ return { pass: false, reason: pythonFailure };
363
+ }
364
+ return { pass: true };
365
+ }
366
+ const CODING_REPAIR_SYSTEM_INSTRUCTION = "Repair the supplied implementation. Return one complete replacement implementation with no prose, " +
367
+ "placeholders, TODOs, omitted sections, or unfinished reasoning. Preserve every requirement in the original task.";
368
+ const CODING_REPAIR_GUIDANCE = {
369
+ code_placeholder: "Replace every placeholder or omitted section with complete executable code.",
370
+ code_not_implemented: "Replace NotImplemented stubs with the complete requested behavior.",
371
+ code_unfinished_reasoning: "Remove unfinished reasoning and finish the implementation before returning it.",
372
+ code_shape_missing: "Return actual source code with the requested definition, not a prose description.",
373
+ code_required_symbol_missing: "Define the exact class, function, method, or interface name requested by the task.",
374
+ python_syntax_error: "Make the entire Python module parse successfully; check every def signature, delimiter, and block.",
375
+ python_method_missing_receiver: "Instance methods must take self first; class methods must take cls first unless decorated staticmethod.",
376
+ python_undefined_private_helper: "Define every directly called private self helper or replace the call with the correct defined helper.",
377
+ constructor_attribute_missing_receiver: "In __init__, persist instance state as self.<attribute>; do not assign it to a discarded local variable.",
378
+ dict_keys_unpack: "When unpacking key and value, iterate dictionary .items(); .keys() yields one key per iteration.",
379
+ };
380
+ function repairGuidance(reason) {
381
+ const guidance = new Set();
382
+ for (const [code, instruction] of Object.entries(CODING_REPAIR_GUIDANCE)) {
383
+ if (reason.includes(code))
384
+ guidance.add(instruction);
385
+ }
386
+ return [...guidance];
387
+ }
388
+ export function buildCodingRepairPrompt(originalPrompt, draft, reason) {
389
+ const safeDraft = draft.replace(/<\|/g, "< |");
390
+ const guidance = repairGuidance(reason);
391
+ return {
392
+ system: CODING_REPAIR_SYSTEM_INSTRUCTION,
393
+ prompt: `<original_task>\n${originalPrompt}\n</original_task>\n` +
394
+ `<failed_gate>${reason}</failed_gate>\n` +
395
+ (guidance.length > 0
396
+ ? `<repair_guidance>\n- ${guidance.join("\n- ")}\n</repair_guidance>\n`
397
+ : "") +
398
+ `<draft>\n${safeDraft}\n</draft>`,
399
+ };
400
+ }
@@ -21,7 +21,8 @@
21
21
  *
22
22
  * Must not run when mode="route" and max_tokens<=16 — that IS a Layer 1 call.
23
23
  */
24
- // VERBATIM — §E prism-infer-boundaries/SKILL.md. Do not edit without re-running eval-layer1.mjs.
24
+ // VERBATIM — §E prism-infer-boundaries/SKILL.md.
25
+ // Changes require the private Layer 1 evaluation gate before release.
25
26
  export const LAYER1_PROMPT = `Classify this request with exactly one word: OBVIOUS_RESERVED, OBVIOUS_NOT_RESERVED, or UNCERTAIN.
26
27
 
27
28
  OBVIOUS_RESERVED — clearly involves:
@@ -33,10 +34,11 @@ OBVIOUS_RESERVED — clearly involves:
33
34
  - Writing auth/login/JWT/token/session validation code
34
35
  - Determining if code bypasses auth or allows unauthenticated access
35
36
  - Assessing whether code actually exposes PHI or constitutes a HIPAA violation
36
- - Whether to push, ship, deploy, or block a production release
37
- - Diagnosis code assignment
37
+ - Whether to push, ship, deploy, block, stop shipping, or make a production go/no-go decision
38
+ - Medication choice, prescribing, or dosing
39
+ - Diagnosis code assignment or professional sign-off
38
40
 
39
- OBVIOUS_NOT_RESERVED — general coding, code review, analysis, documentation, test generation, data processing.
41
+ OBVIOUS_NOT_RESERVED — general coding, code review, analysis, documentation, test generation, or data processing. Also routine non-crisis BCBA measurement and operational definitions; tentative ABC/FBA hypotheses; data gathering before intervention when no injury or imminent danger is stated; educational explanations of function-matched intervention concepts; dignity-preserving language; caregiver training; AAC access rights and replacement-response candidates for BCBA review; and explaining continued AAC access or nonrestrictive positioning when no injury is reported.
40
42
 
41
43
  UNCERTAIN — if the request touches reserved vocabulary but the task is non-security, non-clinical operational work (e.g., deleting or renaming a file, adding a non-auth data field to a form, reviewing existing code for dead exports or hook order) rather than writing clinical safety protocols, implementing auth or security code, making push or deploy decisions, or determining compliance.
42
44
 
@@ -64,6 +66,58 @@ export function parseLayer1(raw) {
64
66
  const token = m[0];
65
67
  return VALID.has(token) ? token : "ERROR";
66
68
  }
69
+ const has = (pattern) => (prompt) => pattern.test(prompt);
70
+ const all = (...rules) => (prompt) => rules.every((rule) => rule(prompt));
71
+ const RESERVED_INTENT_RULES = [
72
+ all(has(/\b(?:de[- ]?escalat\w*|meltdown\w*|rage\s+episode|violent\w*)\b/i), has(/\b(?:draft|write|plan|procedure|protocol|manag\w*|what\s+(?:do|should)\b|respond\w*)\b/i)),
73
+ all(has(/\b(?:physical\s+intervention|containment|hold\s+procedure|restrain\w*|seclu\w*|physical\s+management)\b/i), has(/\b(?:draft|write|document|procedure|protocol|use|implement|instruct\w*)\b/i)),
74
+ all(has(/\b(?:self[- ]?(?:harm|injur\w*)|bites?\s+(?:him|her|them)self|scratches?\s+(?:him|her|them)self|bangs?\s+(?:his|her|their)\s+head|harm\s+(?:himself|herself|themselves))\b/i), has(/\b(?:bleed\w*|blood|concrete|head\s+impact|medical\s+attention|risk(?:\s+of\s+injury)?|causes?\s+injury|actual\s+injury|assess\w*|screen\w*|want\w*)\b/i)),
75
+ (prompt) => (/\b(?:punch\w*|attack\w*|aggress\w*|hit\s+staff)\b/i.test(prompt)
76
+ && /\b(?:last\s+(?:week|month|year)|prior|previous|history|injur\w*)\b/i.test(prompt)
77
+ && !/\bno\s+injur(?:y|ies)\b/i.test(prompt)
78
+ && /\b(?:plan|procedure|intervention|assess\w*)\b/i.test(prompt)),
79
+ has(/\b(?:suicid\w*|homicid\w*)\b/i),
80
+ all(has(/\b(?:write|implement|create|build|add|modify|update|fix|refactor)\b/i), has(/\b(?:auth\w*|login|jwt|tokens?|sessions?|api\s+keys?)\b/i), has(/\b(?:verify|verification|validat\w*|check\w*|middleware|handler)\b/i)),
81
+ all(has(/\b(?:does|can|could|whether|write|implement|create|build|add|modify|update|fix|refactor)\b/i), has(/\b(?:endpoint|route|user|someone|permissions?|authenticat\w*|authoriz\w*)\b/i), has(/\b(?:without|bypass\w*|unauthenticated|not\s+check\w*|lets?\s+anyone|anyone\s+in)\b/i)),
82
+ all(has(/\b(?:ship\w*|deploy\w*|prod(?:uction)?|release\w*)\b/i), has(/\b(?:safe|bad\s+enough|finding\w*|go\s+to|stop|block|ready|whether|can\s+we|should\s+we)\b/i)),
83
+ all(has(/\b(?:expos\w*|intercept\w*|leak\w*|access\w*)\b/i), has(/\b(?:phi|patient\s+(?:records?|data)|health\s+(?:records?|data))\b/i)),
84
+ all(has(/\b(?:medicat\w*|prescrib\w*|dos(?:e|age|ing))\b/i), has(/\b(?:choose|recommend\w*|select|schedule|mg|how\s+much)\b/i)),
85
+ all(has(/\b(?:diagnos\w*|icd[- ]?\d*)\b/i), has(/\b(?:assign|choose|sign[- ]?off|approve|determine)\b/i)),
86
+ ];
87
+ const ROUTINE_BCBA_INTENT_RULES = [
88
+ has(/\boperational\s+definition\b/i),
89
+ all(has(/\bdefin\w*\b/i), has(/\b(?:onset|offset|observers?|score|measur\w*)\b/i)),
90
+ all(has(/\b(?:abc|fba|functional\s+behavior)\b/i), has(/\b(?:hypothes\w*|tentative|summari[sz]\w*)\b/i)),
91
+ all(has(/\b(?:what\s+data|data\s+(?:should|to)\s+(?:be\s+)?gather\w*|collect\s+data)\b/i), has(/\bbefore\s+(?:select\w*|choos\w*|design\w*)\s+(?:an?\s+)?intervention\b/i)),
92
+ all(has(/\b(?:aac|augmentative\s+communication)\b/i), has(/\b(?:replacement[- ]response|replacement\s+(?:skill|behavior)|function[- ]matched)\b/i), has(/\b(?:bcba|clinician)\s+review\b/i)),
93
+ all(has(/\b(?:explain|educat\w*|why)\b/i), has(/\b(?:dro|differential\s+reinforcement|function[- ]matched|maintain\w+\s+by|escape\s+from)\b/i)),
94
+ all(has(/\b(?:caregiver|staff|parent)\s+training\b/i), has(/\b(?:aac|replacement|break[- ]request|communication)\b/i)),
95
+ all(has(/\b(?:rewrite|rephrase)\b/i), has(/\b(?:stigmat\w*|dignity|objective|tentative|function[- ]based)\b/i)),
96
+ all(has(/\b(?:aac|communication\s+device)\b/i), has(/\b(?:remain|keep)\s+available\b/i), has(/\bno\s+injur(?:y|ies)\b/i), has(/\bnonrestrictive\b/i)),
97
+ ];
98
+ function matchesAny(prompt, rules) {
99
+ return rules.some((rule) => rule(prompt));
100
+ }
101
+ const NON_OPERATIONAL_ARTIFACT_CONTEXT = /(?:\b(?:test\s+fixture|fixture\s+label|unit\s+test|old\s+comment|fields?|columns?|labels?|filename|file\s+name|docs?|legal\s+(?:label|phrase|clause)|hook\s+order|dead\s+exports?|type\s+annotation|table\s+scan|add\s+index)\b|\/docs\/|\.[cm]?[jt]sx?\b)/i;
102
+ const NON_OPERATIONAL_ARTIFACT_ACTION = /\b(?:review\b[\s\S]{0,120}\bhook\s+order|delete\b[\s\S]{0,120}\bdocs?|unit\s+tests?|add\b[\s\S]{0,120}\b(?:fields?|index|labels?|numeric\s+validation)|remove\b[\s\S]{0,120}\bcomments?|old\s+comment\b[\s\S]{0,120}\bremove)\b/i;
103
+ /**
104
+ * Deterministic policy floor for unambiguous intents.
105
+ *
106
+ * Routing is code, not model judgment: clear reserved work fails closed and
107
+ * clear routine BCBA work reaches local inference. Ambiguous prompts return
108
+ * null and continue to the semantic classifier below.
109
+ */
110
+ export function classifyDeterministicLayer1(userPrompt) {
111
+ if (matchesAny(userPrompt, RESERVED_INTENT_RULES))
112
+ return "OBVIOUS_RESERVED";
113
+ if (NON_OPERATIONAL_ARTIFACT_CONTEXT.test(userPrompt) &&
114
+ NON_OPERATIONAL_ARTIFACT_ACTION.test(userPrompt)) {
115
+ return "OBVIOUS_NOT_RESERVED";
116
+ }
117
+ if (matchesAny(userPrompt, ROUTINE_BCBA_INTENT_RULES))
118
+ return "OBVIOUS_NOT_RESERVED";
119
+ return null;
120
+ }
67
121
  const LAYER1_TIMEOUT_MS = 1_500;
68
122
  const LAYER1_RETRY_TIMEOUT_MS = 5_000;
69
123
  // Deterministic reserved-vocabulary backstop for the ERROR path.
@@ -123,6 +177,11 @@ export async function callLayer1(userPrompt, ollamaUrl, model, fetchImpl = fetch
123
177
  if (!userPrompt || !userPrompt.trim())
124
178
  return "ERROR";
125
179
  const oversize = userPrompt.length > MAX_CLASSIFIER_PROMPT_LENGTH;
180
+ const deterministic = classifyDeterministicLayer1(userPrompt);
181
+ if (deterministic === "OBVIOUS_RESERVED")
182
+ return deterministic;
183
+ if (!oversize && deterministic === "OBVIOUS_NOT_RESERVED")
184
+ return deterministic;
126
185
  if (oversize && keywordBackstop(userPrompt) === "OBVIOUS_RESERVED") {
127
186
  // The regex floor has no length limit — reserved vocabulary anywhere
128
187
  // in the full text (including the middle the excerpt can't see)