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.
- package/README.md +82 -104
- package/dist/session/sessionContext.js +113 -15
- package/dist/storage/configStorage.js +100 -0
- package/dist/tools/graphHandlers.js +5 -4
- package/dist/tools/handlers.js +2 -2
- package/dist/tools/ledgerHandlers.js +12 -16
- package/dist/tools/prismInferHandler.js +81 -7
- package/dist/tools/queryMemoryNaturalHandler.js +514 -0
- package/dist/tools/sessionMemoryDefinitions.js +23 -5
- package/dist/tools/v12Handlers.js +2 -46
- package/dist/utils/braveApi.js +31 -48
- package/dist/utils/codingQualityPolicy.js +400 -0
- package/dist/utils/layer1.js +63 -4
- package/dist/utils/nlQuery.js +6 -33
- package/dist/utils/qualityGate.js +16 -2
- package/dist/utils/synaluxSearch.js +12 -8
- package/package.json +3 -2
- package/dist/boundaries/__tests__/boundaries.test.js +0 -46
- package/dist/session/__tests__/sessionContext.test.js +0 -134
- package/dist/tools/__tests__/ingestHandler.test.js +0 -323
- package/dist/tools/__tests__/layer1Integration.test.js +0 -590
- package/dist/tools/__tests__/ledgerHandlers.test.js +0 -1281
package/dist/utils/braveApi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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":
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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":
|
|
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":
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/utils/layer1.js
CHANGED
|
@@ -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.
|
|
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
|
|
37
|
-
-
|
|
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)
|