criterionx-vscode 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Pure validation functions for Criterion decision files
3
+ * Extracted from extension.ts for testability
4
+ */
5
+
6
+ export interface Diagnostic {
7
+ range: { start: number; end: number };
8
+ message: string;
9
+ severity: "error" | "warning" | "info";
10
+ }
11
+
12
+ /**
13
+ * Find the end of a block starting with {
14
+ */
15
+ export function findBlockEnd(text: string, start: number): number {
16
+ let depth = 0;
17
+ let inString = false;
18
+ let stringChar = "";
19
+
20
+ for (let i = start; i < text.length; i++) {
21
+ const char = text[i];
22
+ const prevChar = i > 0 ? text[i - 1] : "";
23
+
24
+ if (inString) {
25
+ if (char === stringChar && prevChar !== "\\") {
26
+ inString = false;
27
+ }
28
+ continue;
29
+ }
30
+
31
+ if (char === '"' || char === "'" || char === "`") {
32
+ inString = true;
33
+ stringChar = char;
34
+ continue;
35
+ }
36
+
37
+ if (char === "{") {
38
+ depth++;
39
+ } else if (char === "}") {
40
+ depth--;
41
+ if (depth === 0) {
42
+ return i + 1;
43
+ }
44
+ }
45
+ }
46
+
47
+ return text.length;
48
+ }
49
+
50
+ /**
51
+ * Find the end of an array starting with [
52
+ */
53
+ export function findArrayEnd(text: string, start: number): number {
54
+ let depth = 0;
55
+ let inString = false;
56
+ let stringChar = "";
57
+
58
+ for (let i = start; i < text.length; i++) {
59
+ const char = text[i];
60
+ const prevChar = i > 0 ? text[i - 1] : "";
61
+
62
+ if (inString) {
63
+ if (char === stringChar && prevChar !== "\\") {
64
+ inString = false;
65
+ }
66
+ continue;
67
+ }
68
+
69
+ if (char === '"' || char === "'" || char === "`") {
70
+ inString = true;
71
+ stringChar = char;
72
+ continue;
73
+ }
74
+
75
+ if (char === "[") {
76
+ depth++;
77
+ } else if (char === "]") {
78
+ depth--;
79
+ if (depth === 0) {
80
+ return i;
81
+ }
82
+ }
83
+ }
84
+
85
+ return text.length;
86
+ }
87
+
88
+ /**
89
+ * Check if text content is a Criterion file
90
+ */
91
+ export function isCriterionContent(fileName: string, text: string): boolean {
92
+ return (
93
+ fileName.endsWith(".criterion.ts") ||
94
+ (fileName.endsWith(".ts") &&
95
+ (text.includes("defineDecision") || text.includes("@criterionx/core")))
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Check for missing id in decision
101
+ */
102
+ export function checkMissingId(text: string): Diagnostic[] {
103
+ const diagnostics: Diagnostic[] = [];
104
+
105
+ const defineDecisionRegex = /defineDecision\s*\(\s*\{/g;
106
+ let match;
107
+
108
+ while ((match = defineDecisionRegex.exec(text)) !== null) {
109
+ const startPos = match.index;
110
+ const blockEnd = findBlockEnd(text, startPos);
111
+ const block = text.slice(startPos, blockEnd);
112
+
113
+ if (!block.includes("id:") && !block.includes("id :")) {
114
+ diagnostics.push({
115
+ range: { start: startPos, end: startPos + 14 },
116
+ message: "Decision is missing required 'id' property",
117
+ severity: "error",
118
+ });
119
+ }
120
+ }
121
+
122
+ return diagnostics;
123
+ }
124
+
125
+ /**
126
+ * Check for missing version in decision
127
+ */
128
+ export function checkMissingVersion(text: string): Diagnostic[] {
129
+ const diagnostics: Diagnostic[] = [];
130
+
131
+ const defineDecisionRegex = /defineDecision\s*\(\s*\{/g;
132
+ let match;
133
+
134
+ while ((match = defineDecisionRegex.exec(text)) !== null) {
135
+ const startPos = match.index;
136
+ const blockEnd = findBlockEnd(text, startPos);
137
+ const block = text.slice(startPos, blockEnd);
138
+
139
+ if (!block.includes("version:") && !block.includes("version :")) {
140
+ diagnostics.push({
141
+ range: { start: startPos, end: startPos + 14 },
142
+ message: "Decision is missing required 'version' property",
143
+ severity: "error",
144
+ });
145
+ }
146
+ }
147
+
148
+ return diagnostics;
149
+ }
150
+
151
+ /**
152
+ * Check for missing schemas
153
+ */
154
+ export function checkMissingSchemas(text: string): Diagnostic[] {
155
+ const diagnostics: Diagnostic[] = [];
156
+
157
+ const defineDecisionRegex = /defineDecision\s*\(\s*\{/g;
158
+ let match;
159
+
160
+ while ((match = defineDecisionRegex.exec(text)) !== null) {
161
+ const startPos = match.index;
162
+ const blockEnd = findBlockEnd(text, startPos);
163
+ const block = text.slice(startPos, blockEnd);
164
+
165
+ const schemas = ["inputSchema", "outputSchema", "profileSchema"];
166
+
167
+ for (const schema of schemas) {
168
+ if (!block.includes(`${schema}:`)) {
169
+ diagnostics.push({
170
+ range: { start: startPos, end: startPos + 14 },
171
+ message: `Decision is missing required '${schema}' property`,
172
+ severity: "error",
173
+ });
174
+ }
175
+ }
176
+ }
177
+
178
+ return diagnostics;
179
+ }
180
+
181
+ /**
182
+ * Check for empty rules array
183
+ */
184
+ export function checkEmptyRules(text: string): Diagnostic[] {
185
+ const diagnostics: Diagnostic[] = [];
186
+
187
+ const emptyRulesRegex = /rules\s*:\s*\[\s*\]/g;
188
+ let match;
189
+
190
+ while ((match = emptyRulesRegex.exec(text)) !== null) {
191
+ diagnostics.push({
192
+ range: { start: match.index, end: match.index + match[0].length },
193
+ message: "Decision has no rules defined. Add at least one rule.",
194
+ severity: "warning",
195
+ });
196
+ }
197
+
198
+ return diagnostics;
199
+ }
200
+
201
+ /**
202
+ * Check for missing rule properties
203
+ */
204
+ export function checkMissingRuleProperties(text: string): Diagnostic[] {
205
+ const diagnostics: Diagnostic[] = [];
206
+
207
+ const rulesRegex = /rules\s*:\s*\[/g;
208
+ let rulesMatch;
209
+
210
+ while ((rulesMatch = rulesRegex.exec(text)) !== null) {
211
+ const rulesStart = rulesMatch.index + rulesMatch[0].length;
212
+ const rulesEnd = findArrayEnd(text, rulesMatch.index);
213
+ const rulesBlock = text.slice(rulesStart, rulesEnd);
214
+
215
+ const ruleRegex = /\{\s*id\s*:/g;
216
+ let ruleMatch;
217
+
218
+ while ((ruleMatch = ruleRegex.exec(rulesBlock)) !== null) {
219
+ const ruleStart = rulesStart + ruleMatch.index;
220
+ const ruleEnd = findBlockEnd(text, ruleStart);
221
+ const ruleBlock = text.slice(ruleStart, ruleEnd);
222
+
223
+ const requiredProps = ["when", "emit", "explain"];
224
+
225
+ for (const prop of requiredProps) {
226
+ if (!ruleBlock.includes(`${prop}:`)) {
227
+ diagnostics.push({
228
+ range: { start: ruleStart, end: ruleStart + 1 },
229
+ message: `Rule is missing required '${prop}' function`,
230
+ severity: "error",
231
+ });
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ return diagnostics;
238
+ }
239
+
240
+ /**
241
+ * Validate all checks on text content
242
+ */
243
+ export function validateCriterionContent(text: string): Diagnostic[] {
244
+ return [
245
+ ...checkMissingId(text),
246
+ ...checkMissingVersion(text),
247
+ ...checkMissingSchemas(text),
248
+ ...checkEmptyRules(text),
249
+ ...checkMissingRuleProperties(text),
250
+ ];
251
+ }
252
+
253
+ /**
254
+ * Hover documentation for Criterion keywords
255
+ */
256
+ export const hoverDocs: Record<string, string> = {
257
+ defineDecision:
258
+ "**defineDecision(config)**\n\nCreate a new Criterion decision with type inference.\n\n```typescript\ndefineDecision({\n id: string,\n version: string,\n inputSchema: ZodSchema,\n outputSchema: ZodSchema,\n profileSchema: ZodSchema,\n rules: Rule[]\n})\n```",
259
+ inputSchema:
260
+ "**inputSchema**\n\nZod schema defining the input context for this decision.\n\nExample:\n```typescript\ninputSchema: z.object({\n amount: z.number(),\n country: z.string()\n})\n```",
261
+ outputSchema:
262
+ "**outputSchema**\n\nZod schema defining the output of this decision.\n\nExample:\n```typescript\noutputSchema: z.object({\n approved: z.boolean(),\n reason: z.string()\n})\n```",
263
+ profileSchema:
264
+ "**profileSchema**\n\nZod schema defining the profile parameters.\n\nExample:\n```typescript\nprofileSchema: z.object({\n threshold: z.number(),\n blockedCountries: z.array(z.string())\n})\n```",
265
+ when: "**when(ctx, profile)**\n\nCondition function that returns true if this rule should match.\n\n```typescript\nwhen: (ctx, profile) => ctx.amount > profile.threshold\n```",
266
+ emit: "**emit(ctx, profile)**\n\nFunction that returns the output when this rule matches.\n\n```typescript\nemit: (ctx, profile) => ({\n approved: false,\n reason: 'Amount too high'\n})\n```",
267
+ explain:
268
+ "**explain(ctx, profile)**\n\nFunction that returns a human-readable explanation.\n\n```typescript\nexplain: (ctx) => `Amount ${ctx.amount} exceeded limit`\n```",
269
+ };
270
+
271
+ /**
272
+ * Get hover documentation for a word
273
+ */
274
+ export function getHoverDoc(word: string): string | undefined {
275
+ return hoverDocs[word];
276
+ }
277
+
278
+ /**
279
+ * Generate decision template content
280
+ */
281
+ export function generateDecisionTemplate(name: string): string {
282
+ const pascalName = name
283
+ .split("-")
284
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
285
+ .join("");
286
+ const camelName = pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
287
+
288
+ return `import { defineDecision } from "@criterionx/core";
289
+ import { z } from "zod";
290
+
291
+ /**
292
+ * ${pascalName} Decision
293
+ */
294
+ export const ${camelName} = defineDecision({
295
+ id: "${name}",
296
+ version: "1.0.0",
297
+
298
+ inputSchema: z.object({
299
+ // TODO: Define input schema
300
+ value: z.string(),
301
+ }),
302
+
303
+ outputSchema: z.object({
304
+ // TODO: Define output schema
305
+ result: z.string(),
306
+ }),
307
+
308
+ profileSchema: z.object({
309
+ // TODO: Define profile schema
310
+ }),
311
+
312
+ rules: [
313
+ {
314
+ id: "default",
315
+ when: () => true,
316
+ emit: () => ({ result: "OK" }),
317
+ explain: () => "Default rule",
318
+ },
319
+ ],
320
+ });
321
+ `;
322
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "name": "Criterion Injection",
4
+ "scopeName": "source.criterion.injection",
5
+ "injectionSelector": "L:source.ts, L:source.tsx",
6
+ "patterns": [
7
+ {
8
+ "name": "keyword.control.criterion.injection",
9
+ "match": "\\b(defineDecision|createRule|createProfileRegistry)\\b"
10
+ },
11
+ {
12
+ "name": "constant.language.status.criterion.injection",
13
+ "match": "\\b(OK|NO_MATCH|INVALID_INPUT|INVALID_OUTPUT)\\b"
14
+ },
15
+ {
16
+ "name": "support.type.criterion.injection",
17
+ "match": "\\b(Decision|Rule|Profile|Result|Engine|RuleTrace|ResultMeta)\\b"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "name": "Criterion",
4
+ "scopeName": "source.criterion",
5
+ "patterns": [
6
+ { "include": "#comments" },
7
+ { "include": "#strings" },
8
+ { "include": "#criterion-keywords" },
9
+ { "include": "#criterion-functions" },
10
+ { "include": "#zod-schema" },
11
+ { "include": "#rule-properties" },
12
+ { "include": "source.ts" }
13
+ ],
14
+ "repository": {
15
+ "comments": {
16
+ "patterns": [
17
+ {
18
+ "name": "comment.line.double-slash.criterion",
19
+ "match": "//.*$"
20
+ },
21
+ {
22
+ "name": "comment.block.criterion",
23
+ "begin": "/\\*",
24
+ "end": "\\*/"
25
+ }
26
+ ]
27
+ },
28
+ "strings": {
29
+ "patterns": [
30
+ {
31
+ "name": "string.quoted.double.criterion",
32
+ "begin": "\"",
33
+ "end": "\"",
34
+ "patterns": [
35
+ { "name": "constant.character.escape.criterion", "match": "\\\\." }
36
+ ]
37
+ },
38
+ {
39
+ "name": "string.quoted.single.criterion",
40
+ "begin": "'",
41
+ "end": "'",
42
+ "patterns": [
43
+ { "name": "constant.character.escape.criterion", "match": "\\\\." }
44
+ ]
45
+ },
46
+ {
47
+ "name": "string.template.criterion",
48
+ "begin": "`",
49
+ "end": "`",
50
+ "patterns": [
51
+ { "name": "constant.character.escape.criterion", "match": "\\\\." },
52
+ {
53
+ "name": "meta.template.expression.criterion",
54
+ "begin": "\\$\\{",
55
+ "end": "\\}",
56
+ "patterns": [{ "include": "source.ts" }]
57
+ }
58
+ ]
59
+ }
60
+ ]
61
+ },
62
+ "criterion-keywords": {
63
+ "patterns": [
64
+ {
65
+ "name": "keyword.control.criterion",
66
+ "match": "\\b(defineDecision|createRule|createProfileRegistry)\\b"
67
+ },
68
+ {
69
+ "name": "storage.type.criterion",
70
+ "match": "\\b(Decision|Rule|Profile|Result|Engine)\\b"
71
+ },
72
+ {
73
+ "name": "constant.language.status.criterion",
74
+ "match": "\\b(OK|NO_MATCH|INVALID_INPUT|INVALID_OUTPUT)\\b"
75
+ }
76
+ ]
77
+ },
78
+ "criterion-functions": {
79
+ "patterns": [
80
+ {
81
+ "name": "entity.name.function.criterion",
82
+ "match": "\\b(engine\\.run|engine\\.explain)\\b"
83
+ }
84
+ ]
85
+ },
86
+ "zod-schema": {
87
+ "patterns": [
88
+ {
89
+ "name": "support.function.zod.criterion",
90
+ "match": "\\bz\\.(object|string|number|boolean|array|enum|union|literal|optional|nullable|transform|refine|default)\\b"
91
+ }
92
+ ]
93
+ },
94
+ "rule-properties": {
95
+ "patterns": [
96
+ {
97
+ "name": "variable.other.property.criterion",
98
+ "match": "\\b(id|version|inputSchema|outputSchema|profileSchema|rules|meta|when|emit|explain)\\s*:"
99
+ }
100
+ ]
101
+ }
102
+ }
103
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2022"],
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "moduleResolution": "node",
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "rootDir": "src",
16
+ "outDir": "dist"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/*.test.ts"],
6
+ environment: "node",
7
+ },
8
+ });