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,213 @@
1
+ {
2
+ "Define Decision": {
3
+ "prefix": ["decision", "defineDecision", "crit-decision"],
4
+ "description": "Create a new Criterion decision",
5
+ "body": [
6
+ "import { defineDecision } from \"@criterionx/core\";",
7
+ "import { z } from \"zod\";",
8
+ "",
9
+ "export const ${1:myDecision} = defineDecision({",
10
+ " id: \"${2:decision-id}\",",
11
+ " version: \"${3:1.0.0}\",",
12
+ "",
13
+ " inputSchema: z.object({",
14
+ " ${4:field}: z.${5:string}(),",
15
+ " }),",
16
+ "",
17
+ " outputSchema: z.object({",
18
+ " ${6:result}: z.${7:string}(),",
19
+ " }),",
20
+ "",
21
+ " profileSchema: z.object({",
22
+ " ${8:threshold}: z.${9:number}(),",
23
+ " }),",
24
+ "",
25
+ " rules: [",
26
+ " {",
27
+ " id: \"${10:default}\",",
28
+ " when: (ctx, profile) => ${11:true},",
29
+ " emit: (ctx, profile) => ({ ${6:result}: \"${12:value}\" }),",
30
+ " explain: (ctx, profile) => \"${13:Explanation}\",",
31
+ " },",
32
+ " ],",
33
+ "});",
34
+ ""
35
+ ]
36
+ },
37
+ "Create Rule": {
38
+ "prefix": ["rule", "crit-rule"],
39
+ "description": "Create a new rule for a Criterion decision",
40
+ "body": [
41
+ "{",
42
+ " id: \"${1:rule-id}\",",
43
+ " when: (ctx, profile) => ${2:ctx.value > profile.threshold},",
44
+ " emit: (ctx, profile) => ({",
45
+ " ${3:result}: \"${4:value}\",",
46
+ " }),",
47
+ " explain: (ctx, profile) => `${5:Rule matched because \\${ctx.value\\}}`",
48
+ "},"
49
+ ]
50
+ },
51
+ "Create Profile": {
52
+ "prefix": ["profile", "crit-profile"],
53
+ "description": "Create a profile for a Criterion decision",
54
+ "body": [
55
+ "const ${1:defaultProfile} = {",
56
+ " ${2:threshold}: ${3:1000},",
57
+ " ${4:enabled}: ${5:true},",
58
+ "} satisfies z.infer<typeof ${6:myDecision}.profileSchema>;"
59
+ ]
60
+ },
61
+ "Run Decision": {
62
+ "prefix": ["run", "crit-run", "engine.run"],
63
+ "description": "Run a Criterion decision",
64
+ "body": [
65
+ "import { Engine } from \"@criterionx/core\";",
66
+ "",
67
+ "const engine = new Engine();",
68
+ "",
69
+ "const result = engine.run(",
70
+ " ${1:myDecision},",
71
+ " { ${2:input}: ${3:value} },",
72
+ " { profile: ${4:defaultProfile} }",
73
+ ");",
74
+ "",
75
+ "if (result.status === \"OK\") {",
76
+ " console.log(result.data);",
77
+ "}"
78
+ ]
79
+ },
80
+ "Input Schema": {
81
+ "prefix": ["inputSchema", "crit-input"],
82
+ "description": "Define an input schema with Zod",
83
+ "body": [
84
+ "inputSchema: z.object({",
85
+ " ${1:field}: z.${2:string}(),",
86
+ " ${3:amount}: z.${4:number}(),",
87
+ "}),"
88
+ ]
89
+ },
90
+ "Output Schema": {
91
+ "prefix": ["outputSchema", "crit-output"],
92
+ "description": "Define an output schema with Zod",
93
+ "body": [
94
+ "outputSchema: z.object({",
95
+ " ${1:result}: z.${2:string}(),",
96
+ " ${3:score}: z.${4:number}(),",
97
+ "}),"
98
+ ]
99
+ },
100
+ "Profile Schema": {
101
+ "prefix": ["profileSchema", "crit-profile-schema"],
102
+ "description": "Define a profile schema with Zod",
103
+ "body": [
104
+ "profileSchema: z.object({",
105
+ " ${1:threshold}: z.number(),",
106
+ " ${2:enabled}: z.boolean().default(true),",
107
+ "}),"
108
+ ]
109
+ },
110
+ "When Condition": {
111
+ "prefix": ["when", "crit-when"],
112
+ "description": "Create a when condition for a rule",
113
+ "body": [
114
+ "when: (ctx, profile) => ${1:ctx.amount > profile.threshold},"
115
+ ]
116
+ },
117
+ "Emit Output": {
118
+ "prefix": ["emit", "crit-emit"],
119
+ "description": "Create an emit function for a rule",
120
+ "body": [
121
+ "emit: (ctx, profile) => ({",
122
+ " ${1:result}: \"${2:value}\",",
123
+ " ${3:score}: ${4:ctx.amount / profile.threshold},",
124
+ "}),"
125
+ ]
126
+ },
127
+ "Explain Function": {
128
+ "prefix": ["explain", "crit-explain"],
129
+ "description": "Create an explain function for a rule",
130
+ "body": [
131
+ "explain: (ctx, profile) => `${1:Matched because \\${ctx.field\\} is \\${ctx.value\\}}`,"
132
+ ]
133
+ },
134
+ "Decision with Meta": {
135
+ "prefix": ["decision-meta", "crit-decision-full"],
136
+ "description": "Create a decision with metadata",
137
+ "body": [
138
+ "import { defineDecision } from \"@criterionx/core\";",
139
+ "import { z } from \"zod\";",
140
+ "",
141
+ "export const ${1:myDecision} = defineDecision({",
142
+ " id: \"${2:decision-id}\",",
143
+ " version: \"${3:1.0.0}\",",
144
+ "",
145
+ " meta: {",
146
+ " owner: \"${4:team-name}\",",
147
+ " tags: [\"${5:risk}\", \"${6:finance}\"],",
148
+ " tier: \"${7:critical}\",",
149
+ " description: \"${8:Decision description}\",",
150
+ " },",
151
+ "",
152
+ " inputSchema: z.object({",
153
+ " ${9:field}: z.string(),",
154
+ " }),",
155
+ "",
156
+ " outputSchema: z.object({",
157
+ " ${10:result}: z.string(),",
158
+ " }),",
159
+ "",
160
+ " profileSchema: z.object({",
161
+ " ${11:threshold}: z.number(),",
162
+ " }),",
163
+ "",
164
+ " rules: [",
165
+ " $0",
166
+ " ],",
167
+ "});",
168
+ ""
169
+ ]
170
+ },
171
+ "Test Decision": {
172
+ "prefix": ["test-decision", "crit-test"],
173
+ "description": "Create a test for a Criterion decision",
174
+ "body": [
175
+ "import { testDecision } from \"@criterionx/testing\";",
176
+ "import { ${1:myDecision} } from \"./${2:decisions}\";",
177
+ "",
178
+ "const result = testDecision(${1:myDecision}, {",
179
+ " profile: ${3:defaultProfile},",
180
+ " cases: [",
181
+ " {",
182
+ " name: \"${4:test case}\",",
183
+ " input: { ${5:field}: ${6:value} },",
184
+ " expected: { status: \"OK\", ruleId: \"${7:rule-id}\" },",
185
+ " },",
186
+ " ],",
187
+ " expect: {",
188
+ " noUnreachableRules: true,",
189
+ " },",
190
+ "});",
191
+ "",
192
+ "console.log(result.passed ? \"PASSED\" : \"FAILED\");"
193
+ ]
194
+ },
195
+ "Server Setup": {
196
+ "prefix": ["server", "crit-server"],
197
+ "description": "Create a Criterion server",
198
+ "body": [
199
+ "import { createServer } from \"@criterionx/server\";",
200
+ "import { ${1:myDecision} } from \"./${2:decisions}\";",
201
+ "",
202
+ "const server = createServer({",
203
+ " decisions: [${1:myDecision}],",
204
+ " profiles: {",
205
+ " \"${3:decision-id}\": ${4:defaultProfile},",
206
+ " },",
207
+ "});",
208
+ "",
209
+ "server.listen(${5:3000});",
210
+ "console.log(\"Server running on http://localhost:${5:3000}\");"
211
+ ]
212
+ }
213
+ }
@@ -0,0 +1,156 @@
1
+ import * as vscode from "vscode";
2
+ import {
3
+ isCriterionContent,
4
+ validateCriterionContent,
5
+ getHoverDoc,
6
+ generateDecisionTemplate,
7
+ type Diagnostic,
8
+ } from "./validators";
9
+
10
+ /**
11
+ * Criterion VS Code Extension
12
+ *
13
+ * Provides syntax highlighting, snippets, and basic validation
14
+ * for Criterion decision files.
15
+ */
16
+
17
+ let diagnosticCollection: vscode.DiagnosticCollection;
18
+
19
+ export function activate(context: vscode.ExtensionContext) {
20
+ console.log("Criterion extension activated");
21
+
22
+ // Create diagnostic collection for validation errors
23
+ diagnosticCollection = vscode.languages.createDiagnosticCollection("criterion");
24
+ context.subscriptions.push(diagnosticCollection);
25
+
26
+ // Register document change listener for validation
27
+ const config = vscode.workspace.getConfiguration("criterion");
28
+ if (config.get("validate", true)) {
29
+ // Validate on document change
30
+ context.subscriptions.push(
31
+ vscode.workspace.onDidChangeTextDocument((event) => {
32
+ if (isCriterionContent(event.document.fileName, event.document.getText())) {
33
+ validateDocument(event.document);
34
+ }
35
+ })
36
+ );
37
+
38
+ // Validate on document open
39
+ context.subscriptions.push(
40
+ vscode.workspace.onDidOpenTextDocument((document) => {
41
+ if (isCriterionContent(document.fileName, document.getText())) {
42
+ validateDocument(document);
43
+ }
44
+ })
45
+ );
46
+
47
+ // Validate all open documents on activation
48
+ vscode.workspace.textDocuments.forEach((document) => {
49
+ if (isCriterionContent(document.fileName, document.getText())) {
50
+ validateDocument(document);
51
+ }
52
+ });
53
+ }
54
+
55
+ // Register hover provider for Criterion keywords
56
+ context.subscriptions.push(
57
+ vscode.languages.registerHoverProvider(
58
+ ["typescript", "typescriptreact"],
59
+ new CriterionHoverProvider()
60
+ )
61
+ );
62
+
63
+ // Register command to create a new decision
64
+ context.subscriptions.push(
65
+ vscode.commands.registerCommand("criterion.newDecision", createNewDecision)
66
+ );
67
+ }
68
+
69
+ export function deactivate() {
70
+ if (diagnosticCollection) {
71
+ diagnosticCollection.dispose();
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Validate a Criterion document
77
+ */
78
+ function validateDocument(document: vscode.TextDocument): void {
79
+ const text = document.getText();
80
+ const diagnostics = validateCriterionContent(text);
81
+
82
+ const vscodeDiagnostics = diagnostics.map((d) => convertDiagnostic(d, document));
83
+ diagnosticCollection.set(document.uri, vscodeDiagnostics);
84
+ }
85
+
86
+ /**
87
+ * Convert internal diagnostic to VS Code diagnostic
88
+ */
89
+ function convertDiagnostic(diagnostic: Diagnostic, document: vscode.TextDocument): vscode.Diagnostic {
90
+ const startPos = document.positionAt(diagnostic.range.start);
91
+ const endPos = document.positionAt(diagnostic.range.end);
92
+ const range = new vscode.Range(startPos, endPos);
93
+
94
+ const severity =
95
+ diagnostic.severity === "error"
96
+ ? vscode.DiagnosticSeverity.Error
97
+ : diagnostic.severity === "warning"
98
+ ? vscode.DiagnosticSeverity.Warning
99
+ : vscode.DiagnosticSeverity.Information;
100
+
101
+ return new vscode.Diagnostic(range, diagnostic.message, severity);
102
+ }
103
+
104
+ /**
105
+ * Hover provider for Criterion keywords
106
+ */
107
+ class CriterionHoverProvider implements vscode.HoverProvider {
108
+ provideHover(
109
+ document: vscode.TextDocument,
110
+ position: vscode.Position
111
+ ): vscode.Hover | null {
112
+ const range = document.getWordRangeAtPosition(position);
113
+ if (!range) return null;
114
+
115
+ const word = document.getText(range);
116
+ const doc = getHoverDoc(word);
117
+
118
+ if (doc) {
119
+ return new vscode.Hover(new vscode.MarkdownString(doc));
120
+ }
121
+
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Command to create a new decision file
128
+ */
129
+ async function createNewDecision() {
130
+ const name = await vscode.window.showInputBox({
131
+ prompt: "Enter decision name (e.g., risk-assessment)",
132
+ placeHolder: "decision-name",
133
+ });
134
+
135
+ if (!name) return;
136
+
137
+ const fileName = `${name}.criterion.ts`;
138
+ const content = generateDecisionTemplate(name);
139
+
140
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
141
+ if (!workspaceFolder) {
142
+ vscode.window.showErrorMessage("No workspace folder open");
143
+ return;
144
+ }
145
+
146
+ const uri = vscode.Uri.joinPath(workspaceFolder.uri, fileName);
147
+
148
+ try {
149
+ await vscode.workspace.fs.writeFile(uri, Buffer.from(content));
150
+ const doc = await vscode.workspace.openTextDocument(uri);
151
+ await vscode.window.showTextDocument(doc);
152
+ vscode.window.showInformationMessage(`Created ${fileName}`);
153
+ } catch (error) {
154
+ vscode.window.showErrorMessage(`Failed to create file: ${error}`);
155
+ }
156
+ }
@@ -0,0 +1,340 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ findBlockEnd,
4
+ findArrayEnd,
5
+ isCriterionContent,
6
+ checkMissingId,
7
+ checkMissingVersion,
8
+ checkMissingSchemas,
9
+ checkEmptyRules,
10
+ checkMissingRuleProperties,
11
+ validateCriterionContent,
12
+ getHoverDoc,
13
+ generateDecisionTemplate,
14
+ hoverDocs,
15
+ } from "./validators";
16
+
17
+ describe("validators", () => {
18
+ describe("findBlockEnd", () => {
19
+ it("finds end of simple block", () => {
20
+ const text = "{ foo: 1 }";
21
+ expect(findBlockEnd(text, 0)).toBe(10);
22
+ });
23
+
24
+ it("handles nested blocks", () => {
25
+ const text = "{ foo: { bar: 1 } }";
26
+ expect(findBlockEnd(text, 0)).toBe(19);
27
+ });
28
+
29
+ it("handles strings with braces", () => {
30
+ const text = '{ foo: "{ not a block }" }';
31
+ expect(findBlockEnd(text, 0)).toBe(26);
32
+ });
33
+
34
+ it("handles template literals with braces", () => {
35
+ const text = "{ foo: `{ template }` }";
36
+ expect(findBlockEnd(text, 0)).toBe(23);
37
+ });
38
+
39
+ it("handles escaped quotes in strings", () => {
40
+ const text = '{ foo: "escaped \\" quote" }';
41
+ expect(findBlockEnd(text, 0)).toBe(27);
42
+ });
43
+
44
+ it("returns text length if no closing brace", () => {
45
+ const text = "{ foo: 1";
46
+ expect(findBlockEnd(text, 0)).toBe(8);
47
+ });
48
+ });
49
+
50
+ describe("findArrayEnd", () => {
51
+ it("finds end of simple array", () => {
52
+ const text = "[1, 2, 3]";
53
+ expect(findArrayEnd(text, 0)).toBe(8);
54
+ });
55
+
56
+ it("handles nested arrays", () => {
57
+ const text = "[[1, 2], [3, 4]]";
58
+ expect(findArrayEnd(text, 0)).toBe(15);
59
+ });
60
+
61
+ it("handles strings with brackets", () => {
62
+ const text = '["[ not an array ]"]';
63
+ expect(findArrayEnd(text, 0)).toBe(19);
64
+ });
65
+
66
+ it("handles mixed nesting", () => {
67
+ const text = '[{ arr: [1] }, "text"]';
68
+ expect(findArrayEnd(text, 0)).toBe(21);
69
+ });
70
+
71
+ it("returns text length if no closing bracket", () => {
72
+ const text = "[1, 2";
73
+ expect(findArrayEnd(text, 0)).toBe(5);
74
+ });
75
+ });
76
+
77
+ describe("isCriterionContent", () => {
78
+ it("returns true for .criterion.ts files", () => {
79
+ expect(isCriterionContent("risk.criterion.ts", "")).toBe(true);
80
+ });
81
+
82
+ it("returns true for .ts files with defineDecision", () => {
83
+ expect(isCriterionContent("rules.ts", "defineDecision({})")).toBe(true);
84
+ });
85
+
86
+ it("returns true for .ts files with @criterionx/core import", () => {
87
+ expect(
88
+ isCriterionContent("rules.ts", 'import { Engine } from "@criterionx/core"')
89
+ ).toBe(true);
90
+ });
91
+
92
+ it("returns false for non-criterion .ts files", () => {
93
+ expect(isCriterionContent("utils.ts", "function foo() {}")).toBe(false);
94
+ });
95
+
96
+ it("returns false for non-.ts files", () => {
97
+ expect(isCriterionContent("readme.md", "defineDecision")).toBe(false);
98
+ });
99
+ });
100
+
101
+ describe("checkMissingId", () => {
102
+ it("returns error for decision without id", () => {
103
+ const text = `
104
+ defineDecision({
105
+ version: "1.0.0",
106
+ inputSchema: z.object({}),
107
+ outputSchema: z.object({}),
108
+ profileSchema: z.object({}),
109
+ rules: []
110
+ })
111
+ `;
112
+ const diagnostics = checkMissingId(text);
113
+ expect(diagnostics).toHaveLength(1);
114
+ expect(diagnostics[0].message).toBe("Decision is missing required 'id' property");
115
+ expect(diagnostics[0].severity).toBe("error");
116
+ });
117
+
118
+ it("returns empty for decision with id", () => {
119
+ const text = `
120
+ defineDecision({
121
+ id: "my-decision",
122
+ version: "1.0.0"
123
+ })
124
+ `;
125
+ const diagnostics = checkMissingId(text);
126
+ expect(diagnostics).toHaveLength(0);
127
+ });
128
+
129
+ it("handles id with space before colon", () => {
130
+ const text = `defineDecision({ id : "test" })`;
131
+ const diagnostics = checkMissingId(text);
132
+ expect(diagnostics).toHaveLength(0);
133
+ });
134
+ });
135
+
136
+ describe("checkMissingVersion", () => {
137
+ it("returns error for decision without version", () => {
138
+ const text = `
139
+ defineDecision({
140
+ id: "test"
141
+ })
142
+ `;
143
+ const diagnostics = checkMissingVersion(text);
144
+ expect(diagnostics).toHaveLength(1);
145
+ expect(diagnostics[0].message).toBe("Decision is missing required 'version' property");
146
+ });
147
+
148
+ it("returns empty for decision with version", () => {
149
+ const text = `defineDecision({ version: "1.0.0" })`;
150
+ const diagnostics = checkMissingVersion(text);
151
+ expect(diagnostics).toHaveLength(0);
152
+ });
153
+ });
154
+
155
+ describe("checkMissingSchemas", () => {
156
+ it("returns errors for missing schemas", () => {
157
+ const text = `
158
+ defineDecision({
159
+ id: "test",
160
+ version: "1.0.0"
161
+ })
162
+ `;
163
+ const diagnostics = checkMissingSchemas(text);
164
+ expect(diagnostics).toHaveLength(3);
165
+ expect(diagnostics.map((d) => d.message)).toContain(
166
+ "Decision is missing required 'inputSchema' property"
167
+ );
168
+ expect(diagnostics.map((d) => d.message)).toContain(
169
+ "Decision is missing required 'outputSchema' property"
170
+ );
171
+ expect(diagnostics.map((d) => d.message)).toContain(
172
+ "Decision is missing required 'profileSchema' property"
173
+ );
174
+ });
175
+
176
+ it("returns empty for decision with all schemas", () => {
177
+ const text = `
178
+ defineDecision({
179
+ inputSchema: z.object({}),
180
+ outputSchema: z.object({}),
181
+ profileSchema: z.object({})
182
+ })
183
+ `;
184
+ const diagnostics = checkMissingSchemas(text);
185
+ expect(diagnostics).toHaveLength(0);
186
+ });
187
+ });
188
+
189
+ describe("checkEmptyRules", () => {
190
+ it("returns warning for empty rules array", () => {
191
+ const text = `rules: []`;
192
+ const diagnostics = checkEmptyRules(text);
193
+ expect(diagnostics).toHaveLength(1);
194
+ expect(diagnostics[0].message).toBe("Decision has no rules defined. Add at least one rule.");
195
+ expect(diagnostics[0].severity).toBe("warning");
196
+ });
197
+
198
+ it("returns warning for empty rules with whitespace", () => {
199
+ const text = `rules: [ ]`;
200
+ const diagnostics = checkEmptyRules(text);
201
+ expect(diagnostics).toHaveLength(1);
202
+ });
203
+
204
+ it("returns empty for non-empty rules", () => {
205
+ const text = `rules: [{ id: "test" }]`;
206
+ const diagnostics = checkEmptyRules(text);
207
+ expect(diagnostics).toHaveLength(0);
208
+ });
209
+ });
210
+
211
+ describe("checkMissingRuleProperties", () => {
212
+ it("returns errors for rule missing required properties", () => {
213
+ const text = `
214
+ rules: [
215
+ {
216
+ id: "test"
217
+ }
218
+ ]
219
+ `;
220
+ const diagnostics = checkMissingRuleProperties(text);
221
+ expect(diagnostics).toHaveLength(3);
222
+ expect(diagnostics.map((d) => d.message)).toContain("Rule is missing required 'when' function");
223
+ expect(diagnostics.map((d) => d.message)).toContain("Rule is missing required 'emit' function");
224
+ expect(diagnostics.map((d) => d.message)).toContain(
225
+ "Rule is missing required 'explain' function"
226
+ );
227
+ });
228
+
229
+ it("returns empty for complete rule", () => {
230
+ const text = `
231
+ rules: [
232
+ {
233
+ id: "test",
234
+ when: () => true,
235
+ emit: () => ({}),
236
+ explain: () => "test"
237
+ }
238
+ ]
239
+ `;
240
+ const diagnostics = checkMissingRuleProperties(text);
241
+ expect(diagnostics).toHaveLength(0);
242
+ });
243
+ });
244
+
245
+ describe("validateCriterionContent", () => {
246
+ it("returns all diagnostics for invalid decision", () => {
247
+ const text = `
248
+ defineDecision({
249
+ rules: []
250
+ })
251
+ `;
252
+ const diagnostics = validateCriterionContent(text);
253
+ // Missing: id, version, inputSchema, outputSchema, profileSchema + empty rules warning
254
+ expect(diagnostics.length).toBeGreaterThan(0);
255
+ });
256
+
257
+ it("returns empty for valid decision", () => {
258
+ const text = `
259
+ defineDecision({
260
+ id: "test",
261
+ version: "1.0.0",
262
+ inputSchema: z.object({}),
263
+ outputSchema: z.object({}),
264
+ profileSchema: z.object({}),
265
+ rules: [
266
+ {
267
+ id: "default",
268
+ when: () => true,
269
+ emit: () => ({}),
270
+ explain: () => "Default"
271
+ }
272
+ ]
273
+ })
274
+ `;
275
+ const diagnostics = validateCriterionContent(text);
276
+ expect(diagnostics).toHaveLength(0);
277
+ });
278
+ });
279
+
280
+ describe("getHoverDoc", () => {
281
+ it("returns documentation for known keywords", () => {
282
+ expect(getHoverDoc("defineDecision")).toContain("Create a new Criterion decision");
283
+ expect(getHoverDoc("inputSchema")).toContain("Zod schema defining the input");
284
+ expect(getHoverDoc("when")).toContain("Condition function");
285
+ expect(getHoverDoc("emit")).toContain("Function that returns the output");
286
+ expect(getHoverDoc("explain")).toContain("human-readable explanation");
287
+ });
288
+
289
+ it("returns undefined for unknown keywords", () => {
290
+ expect(getHoverDoc("unknownKeyword")).toBeUndefined();
291
+ expect(getHoverDoc("")).toBeUndefined();
292
+ });
293
+ });
294
+
295
+ describe("hoverDocs", () => {
296
+ it("has documentation for all expected keywords", () => {
297
+ const expectedKeywords = [
298
+ "defineDecision",
299
+ "inputSchema",
300
+ "outputSchema",
301
+ "profileSchema",
302
+ "when",
303
+ "emit",
304
+ "explain",
305
+ ];
306
+ for (const keyword of expectedKeywords) {
307
+ expect(hoverDocs[keyword]).toBeDefined();
308
+ }
309
+ });
310
+ });
311
+
312
+ describe("generateDecisionTemplate", () => {
313
+ it("generates valid decision template", () => {
314
+ const template = generateDecisionTemplate("risk-assessment");
315
+ expect(template).toContain('import { defineDecision } from "@criterionx/core"');
316
+ expect(template).toContain('import { z } from "zod"');
317
+ expect(template).toContain("RiskAssessment Decision");
318
+ expect(template).toContain("export const riskAssessment");
319
+ expect(template).toContain('id: "risk-assessment"');
320
+ expect(template).toContain('version: "1.0.0"');
321
+ expect(template).toContain("inputSchema:");
322
+ expect(template).toContain("outputSchema:");
323
+ expect(template).toContain("profileSchema:");
324
+ expect(template).toContain("rules:");
325
+ });
326
+
327
+ it("handles single-word names", () => {
328
+ const template = generateDecisionTemplate("approval");
329
+ expect(template).toContain("Approval Decision");
330
+ expect(template).toContain("export const approval");
331
+ expect(template).toContain('id: "approval"');
332
+ });
333
+
334
+ it("handles multi-word names", () => {
335
+ const template = generateDecisionTemplate("loan-risk-assessment");
336
+ expect(template).toContain("LoanRiskAssessment Decision");
337
+ expect(template).toContain("export const loanRiskAssessment");
338
+ });
339
+ });
340
+ });