@stackmemoryai/stackmemory 1.9.0 → 1.10.2
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/dist/src/cli/commands/orchestrator.js +27 -6
- package/dist/src/cli/commands/ralph.js +43 -0
- package/dist/src/cli/commands/rules.js +250 -0
- package/dist/src/cli/commands/skill.js +406 -0
- package/dist/src/cli/index.js +43 -0
- package/dist/src/core/rules/built-in-rules.js +289 -0
- package/dist/src/core/rules/pr-review-rule.js +87 -0
- package/dist/src/core/rules/rule-engine.js +85 -0
- package/dist/src/core/rules/rule-store.js +99 -0
- package/dist/src/core/rules/types.js +4 -0
- package/dist/src/core/skills/index.js +21 -0
- package/dist/src/core/skills/skill-matcher.js +178 -0
- package/dist/src/core/skills/skill-registry.js +646 -0
- package/dist/src/core/skills/types.js +1 -47
- package/dist/src/integrations/mcp/handlers/skill-handlers.js +67 -167
- package/dist/src/integrations/mcp/server.js +0 -6
- package/dist/src/integrations/ralph/loopmax.js +488 -0
- package/package.json +2 -2
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { prReviewRule } from "./pr-review-rule.js";
|
|
6
|
+
function violation(ruleId, ruleName, severity, message, file, line, suggestion) {
|
|
7
|
+
return { ruleId, ruleName, severity, message, file, line, suggestion };
|
|
8
|
+
}
|
|
9
|
+
function pass() {
|
|
10
|
+
return { passed: true, violations: [] };
|
|
11
|
+
}
|
|
12
|
+
function fail(violations) {
|
|
13
|
+
return { passed: false, violations };
|
|
14
|
+
}
|
|
15
|
+
function escapeGlobPart(part) {
|
|
16
|
+
return part.replace(/\./g, "\\.").replace(
|
|
17
|
+
/\{([^}]+)\}/g,
|
|
18
|
+
(_m, choices) => `(${choices.split(",").join("|")})`
|
|
19
|
+
).replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
20
|
+
}
|
|
21
|
+
function globToRegex(pattern) {
|
|
22
|
+
if (pattern.startsWith("**/")) {
|
|
23
|
+
const rest = escapeGlobPart(pattern.slice(3));
|
|
24
|
+
return new RegExp(`^(?:.+/)?${rest}$`);
|
|
25
|
+
}
|
|
26
|
+
const parts = pattern.split("/**/");
|
|
27
|
+
if (parts.length === 1) {
|
|
28
|
+
return new RegExp(`^${escapeGlobPart(pattern)}$`);
|
|
29
|
+
}
|
|
30
|
+
const regexParts = parts.map(escapeGlobPart);
|
|
31
|
+
return new RegExp(`^${regexParts.join("(?:/.*?/|/)")}$`);
|
|
32
|
+
}
|
|
33
|
+
function matchesScope(filePath, scope) {
|
|
34
|
+
if (scope === "**/*" || scope === "*") return true;
|
|
35
|
+
const re = globToRegex(scope);
|
|
36
|
+
return re.test(filePath);
|
|
37
|
+
}
|
|
38
|
+
function filterByScope(files, scope) {
|
|
39
|
+
return files.filter((f) => matchesScope(f, scope));
|
|
40
|
+
}
|
|
41
|
+
const noCoauthor = {
|
|
42
|
+
id: "no-coauthor",
|
|
43
|
+
name: "No Co-Authored-By",
|
|
44
|
+
description: "Block Co-Authored-By lines in commit messages",
|
|
45
|
+
trigger: "commit",
|
|
46
|
+
severity: "error",
|
|
47
|
+
scope: "*",
|
|
48
|
+
enabled: true,
|
|
49
|
+
builtin: true,
|
|
50
|
+
check(ctx) {
|
|
51
|
+
if (!ctx.commitMessage) return pass();
|
|
52
|
+
if (/co-authored-by/i.test(ctx.commitMessage)) {
|
|
53
|
+
return fail([
|
|
54
|
+
violation(
|
|
55
|
+
this.id,
|
|
56
|
+
this.name,
|
|
57
|
+
this.severity,
|
|
58
|
+
"Commit message contains Co-Authored-By line",
|
|
59
|
+
void 0,
|
|
60
|
+
void 0,
|
|
61
|
+
"Remove the Co-Authored-By trailer"
|
|
62
|
+
)
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
return pass();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const noJestGlobals = {
|
|
69
|
+
id: "no-jest-globals",
|
|
70
|
+
name: "No @jest/globals imports",
|
|
71
|
+
description: "Flag @jest/globals imports in src/ tests (causes redeclaration errors)",
|
|
72
|
+
trigger: "lint",
|
|
73
|
+
severity: "error",
|
|
74
|
+
scope: "src/**/*.test.{ts,js}",
|
|
75
|
+
enabled: true,
|
|
76
|
+
builtin: true,
|
|
77
|
+
check(ctx) {
|
|
78
|
+
const violations = [];
|
|
79
|
+
const files = filterByScope(ctx.files, this.scope);
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const content = ctx.content.get(file);
|
|
82
|
+
if (!content) continue;
|
|
83
|
+
const lines = content.split("\n");
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
if (line && /@jest\/globals/.test(line)) {
|
|
87
|
+
violations.push(
|
|
88
|
+
violation(
|
|
89
|
+
this.id,
|
|
90
|
+
this.name,
|
|
91
|
+
this.severity,
|
|
92
|
+
`Import from @jest/globals found \u2014 use global jest instead`,
|
|
93
|
+
file,
|
|
94
|
+
i + 1,
|
|
95
|
+
"Remove the import; jest/describe/it/expect are globally available"
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return violations.length > 0 ? fail(violations) : pass();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const catchNoUnderscore = {
|
|
105
|
+
id: "catch-no-underscore",
|
|
106
|
+
name: "Catch without underscore prefix",
|
|
107
|
+
description: "Enforce catch {} not catch (_err) {} \u2014 underscore prefix not in allowed ESLint pattern",
|
|
108
|
+
trigger: "lint",
|
|
109
|
+
severity: "warn",
|
|
110
|
+
scope: "src/**/*.{ts,js}",
|
|
111
|
+
enabled: true,
|
|
112
|
+
builtin: true,
|
|
113
|
+
check(ctx) {
|
|
114
|
+
const violations = [];
|
|
115
|
+
const files = filterByScope(ctx.files, this.scope);
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const content = ctx.content.get(file);
|
|
118
|
+
if (!content) continue;
|
|
119
|
+
const lines = content.split("\n");
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
const line = lines[i];
|
|
122
|
+
if (line && /catch\s*\(\s*_\w*\s*\)/.test(line)) {
|
|
123
|
+
violations.push(
|
|
124
|
+
violation(
|
|
125
|
+
this.id,
|
|
126
|
+
this.name,
|
|
127
|
+
this.severity,
|
|
128
|
+
"catch with underscore-prefixed variable",
|
|
129
|
+
file,
|
|
130
|
+
i + 1,
|
|
131
|
+
"Use catch {} (empty) or catch (err) {} (without underscore)"
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return violations.length > 0 ? fail(violations) : pass();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const THROW_EXCLUDE_PATTERNS = [
|
|
141
|
+
/middleware/i,
|
|
142
|
+
/errors?\//i,
|
|
143
|
+
/errors?\.(ts|js)$/i,
|
|
144
|
+
/index\.(ts|js)$/,
|
|
145
|
+
/\.test\.(ts|js)$/,
|
|
146
|
+
/__tests__/
|
|
147
|
+
];
|
|
148
|
+
const returnDontThrow = {
|
|
149
|
+
id: "return-dont-throw",
|
|
150
|
+
name: "Return undefined over throw",
|
|
151
|
+
description: "Warn on throw in non-boundary code \u2014 prefer return undefined + log",
|
|
152
|
+
trigger: "lint",
|
|
153
|
+
severity: "info",
|
|
154
|
+
scope: "src/**/*.{ts,js}",
|
|
155
|
+
enabled: true,
|
|
156
|
+
builtin: true,
|
|
157
|
+
check(ctx) {
|
|
158
|
+
const violations = [];
|
|
159
|
+
const files = filterByScope(ctx.files, this.scope);
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
if (THROW_EXCLUDE_PATTERNS.some((p) => p.test(file))) continue;
|
|
162
|
+
const content = ctx.content.get(file);
|
|
163
|
+
if (!content) continue;
|
|
164
|
+
const lines = content.split("\n");
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
const line = lines[i];
|
|
167
|
+
if (line && /throw\s+new\s+/.test(line)) {
|
|
168
|
+
violations.push(
|
|
169
|
+
violation(
|
|
170
|
+
this.id,
|
|
171
|
+
this.name,
|
|
172
|
+
this.severity,
|
|
173
|
+
"throw statement in non-boundary code",
|
|
174
|
+
file,
|
|
175
|
+
i + 1,
|
|
176
|
+
"Consider returning undefined and logging the error instead"
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return violations.length > 0 ? fail(violations) : pass();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const migrationSequential = {
|
|
186
|
+
id: "migration-sequential",
|
|
187
|
+
name: "Sequential migration numbering",
|
|
188
|
+
description: "Validate migration files have no numbering gaps",
|
|
189
|
+
trigger: "on-demand",
|
|
190
|
+
severity: "error",
|
|
191
|
+
scope: "**/migrations/*.sql",
|
|
192
|
+
enabled: true,
|
|
193
|
+
builtin: true,
|
|
194
|
+
check(ctx) {
|
|
195
|
+
const files = filterByScope(ctx.files, this.scope);
|
|
196
|
+
const numbers = [];
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
const basename = file.split("/").pop() ?? "";
|
|
199
|
+
const match = /^(\d+)/.exec(basename);
|
|
200
|
+
if (match?.[1]) {
|
|
201
|
+
numbers.push(parseInt(match[1], 10));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (numbers.length < 2) return pass();
|
|
205
|
+
numbers.sort((a, b) => a - b);
|
|
206
|
+
const violations = [];
|
|
207
|
+
for (let i = 1; i < numbers.length; i++) {
|
|
208
|
+
const prev = numbers[i - 1];
|
|
209
|
+
const curr = numbers[i];
|
|
210
|
+
if (curr - prev > 1) {
|
|
211
|
+
violations.push(
|
|
212
|
+
violation(
|
|
213
|
+
this.id,
|
|
214
|
+
this.name,
|
|
215
|
+
this.severity,
|
|
216
|
+
`Migration gap: ${String(prev).padStart(3, "0")} \u2192 ${String(curr).padStart(3, "0")} (missing ${curr - prev - 1} file(s))`,
|
|
217
|
+
void 0,
|
|
218
|
+
void 0,
|
|
219
|
+
`Add migration(s) for numbers ${prev + 1}\u2013${curr - 1}`
|
|
220
|
+
)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return violations.length > 0 ? fail(violations) : pass();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const mockLifecycle = {
|
|
228
|
+
id: "mock-lifecycle",
|
|
229
|
+
name: "Mock lifecycle in tests",
|
|
230
|
+
description: "Warn if clearAllMocks() is called without re-setting mocks in beforeEach",
|
|
231
|
+
trigger: "lint",
|
|
232
|
+
severity: "warn",
|
|
233
|
+
scope: "src/**/*.test.{ts,js}",
|
|
234
|
+
enabled: true,
|
|
235
|
+
builtin: true,
|
|
236
|
+
check(ctx) {
|
|
237
|
+
const violations = [];
|
|
238
|
+
const files = filterByScope(ctx.files, this.scope);
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
const content = ctx.content.get(file);
|
|
241
|
+
if (!content) continue;
|
|
242
|
+
const hasClearAll = /clearAllMocks\(\)/.test(content);
|
|
243
|
+
if (!hasClearAll) continue;
|
|
244
|
+
const hasBeforeEach = /beforeEach/.test(content);
|
|
245
|
+
const hasMockSetup = /mock(ReturnValue|ResolvedValue|Implementation)\s*\(/.test(content);
|
|
246
|
+
if (hasClearAll && hasBeforeEach && !hasMockSetup) {
|
|
247
|
+
violations.push(
|
|
248
|
+
violation(
|
|
249
|
+
this.id,
|
|
250
|
+
this.name,
|
|
251
|
+
this.severity,
|
|
252
|
+
"clearAllMocks() used but no mock re-setup found (mockReturnValue/mockResolvedValue/mockImplementation)",
|
|
253
|
+
file,
|
|
254
|
+
void 0,
|
|
255
|
+
"Re-set mock return values in beforeEach after clearAllMocks resets them"
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return violations.length > 0 ? fail(violations) : pass();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const BUILT_IN_RULES = [
|
|
264
|
+
noCoauthor,
|
|
265
|
+
noJestGlobals,
|
|
266
|
+
catchNoUnderscore,
|
|
267
|
+
returnDontThrow,
|
|
268
|
+
migrationSequential,
|
|
269
|
+
mockLifecycle,
|
|
270
|
+
prReviewRule
|
|
271
|
+
];
|
|
272
|
+
function getBuiltinRows() {
|
|
273
|
+
return BUILT_IN_RULES.map((r) => ({
|
|
274
|
+
id: r.id,
|
|
275
|
+
name: r.name,
|
|
276
|
+
description: r.description,
|
|
277
|
+
trigger_type: r.trigger,
|
|
278
|
+
severity: r.severity,
|
|
279
|
+
scope: r.scope,
|
|
280
|
+
enabled: r.enabled ? 1 : 0,
|
|
281
|
+
builtin: r.builtin ? 1 : 0
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
export {
|
|
285
|
+
BUILT_IN_RULES,
|
|
286
|
+
filterByScope,
|
|
287
|
+
getBuiltinRows,
|
|
288
|
+
matchesScope
|
|
289
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { filterByScope } from "./built-in-rules.js";
|
|
6
|
+
function violation(ruleId, ruleName, severity, message, file, line, suggestion) {
|
|
7
|
+
return { ruleId, ruleName, severity, message, file, line, suggestion };
|
|
8
|
+
}
|
|
9
|
+
const PR_REVIEW_PATTERNS = [
|
|
10
|
+
{
|
|
11
|
+
id: "console-log",
|
|
12
|
+
pattern: /console\.(log|debug|info)\(/,
|
|
13
|
+
message: "console.log left in code",
|
|
14
|
+
suggestion: "Remove or replace with structured logger",
|
|
15
|
+
severity: "warn"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "todo-fixme",
|
|
19
|
+
pattern: /\/\/\s*(TODO|FIXME|HACK|XXX)(?!\s*\()/,
|
|
20
|
+
message: "TODO/FIXME without attribution",
|
|
21
|
+
suggestion: "Add author and ticket: // TODO(user): STA-XXX description",
|
|
22
|
+
severity: "info"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "hardcoded-secret",
|
|
26
|
+
pattern: /(api[_-]?key|secret|password|token)\s*[:=]\s*['"][^'"]{8,}['"]/i,
|
|
27
|
+
message: "Possible hardcoded secret",
|
|
28
|
+
suggestion: "Move to environment variable",
|
|
29
|
+
severity: "error"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "any-type",
|
|
33
|
+
pattern: /:\s*any\b/,
|
|
34
|
+
message: "Explicit any type",
|
|
35
|
+
suggestion: "Use a specific type or unknown",
|
|
36
|
+
severity: "warn"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "empty-catch",
|
|
40
|
+
pattern: /catch\s*\{[\s]*\}/,
|
|
41
|
+
message: "Empty catch block (swallows errors silently)",
|
|
42
|
+
suggestion: "Log the error or add a comment explaining why it is safe to ignore",
|
|
43
|
+
severity: "warn"
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
const prReviewRule = {
|
|
47
|
+
id: "pr-review-patterns",
|
|
48
|
+
name: "PR Review Patterns",
|
|
49
|
+
description: "Catch common issues that PR reviewers flag (console.log, TODO, secrets, any type)",
|
|
50
|
+
trigger: "pre-commit",
|
|
51
|
+
severity: "warn",
|
|
52
|
+
scope: "src/**/*.{ts,js,tsx,jsx}",
|
|
53
|
+
enabled: true,
|
|
54
|
+
builtin: true,
|
|
55
|
+
check(ctx) {
|
|
56
|
+
const violations = [];
|
|
57
|
+
const files = filterByScope(ctx.files, this.scope);
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const content = ctx.content.get(file);
|
|
60
|
+
if (!content) continue;
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i];
|
|
64
|
+
if (!line) continue;
|
|
65
|
+
for (const check of PR_REVIEW_PATTERNS) {
|
|
66
|
+
if (check.pattern.test(line)) {
|
|
67
|
+
violations.push(
|
|
68
|
+
violation(
|
|
69
|
+
this.id,
|
|
70
|
+
`${this.name}: ${check.id}`,
|
|
71
|
+
check.severity,
|
|
72
|
+
check.message,
|
|
73
|
+
file,
|
|
74
|
+
i + 1,
|
|
75
|
+
check.suggestion
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return violations.length > 0 ? { passed: false, violations } : { passed: true, violations: [] };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
export {
|
|
86
|
+
prReviewRule
|
|
87
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { RuleStore } from "./rule-store.js";
|
|
6
|
+
import {
|
|
7
|
+
BUILT_IN_RULES,
|
|
8
|
+
getBuiltinRows,
|
|
9
|
+
filterByScope
|
|
10
|
+
} from "./built-in-rules.js";
|
|
11
|
+
class RuleEngine {
|
|
12
|
+
store;
|
|
13
|
+
checkFns = /* @__PURE__ */ new Map();
|
|
14
|
+
constructor(db) {
|
|
15
|
+
this.store = new RuleStore(db);
|
|
16
|
+
this.store.seedBuiltins(getBuiltinRows());
|
|
17
|
+
for (const rule of BUILT_IN_RULES) {
|
|
18
|
+
this.checkFns.set(rule.id, rule.check.bind(rule));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
registerRule(rule) {
|
|
22
|
+
this.store.upsert({
|
|
23
|
+
id: rule.id,
|
|
24
|
+
name: rule.name,
|
|
25
|
+
description: rule.description,
|
|
26
|
+
trigger_type: rule.trigger,
|
|
27
|
+
severity: rule.severity,
|
|
28
|
+
scope: rule.scope,
|
|
29
|
+
enabled: rule.enabled ? 1 : 0,
|
|
30
|
+
builtin: rule.builtin ? 1 : 0
|
|
31
|
+
});
|
|
32
|
+
this.checkFns.set(rule.id, rule.check.bind(rule));
|
|
33
|
+
}
|
|
34
|
+
evaluate(context) {
|
|
35
|
+
const rows = this.store.getByTrigger(context.trigger);
|
|
36
|
+
return this.runRules(rows, context);
|
|
37
|
+
}
|
|
38
|
+
evaluateAll(context) {
|
|
39
|
+
const rows = this.store.getEnabled();
|
|
40
|
+
return this.runRules(rows, context);
|
|
41
|
+
}
|
|
42
|
+
listRules(filter) {
|
|
43
|
+
if (filter?.trigger) {
|
|
44
|
+
return this.store.getByTrigger(filter.trigger);
|
|
45
|
+
}
|
|
46
|
+
if (filter?.enabled === false) {
|
|
47
|
+
return this.store.getAll();
|
|
48
|
+
}
|
|
49
|
+
return this.store.getEnabled();
|
|
50
|
+
}
|
|
51
|
+
enableRule(id) {
|
|
52
|
+
return this.store.setEnabled(id, true);
|
|
53
|
+
}
|
|
54
|
+
disableRule(id) {
|
|
55
|
+
return this.store.setEnabled(id, false);
|
|
56
|
+
}
|
|
57
|
+
getStore() {
|
|
58
|
+
return this.store;
|
|
59
|
+
}
|
|
60
|
+
runRules(rows, context) {
|
|
61
|
+
const allViolations = [];
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
const checkFn = this.checkFns.get(row.id);
|
|
64
|
+
if (!checkFn) continue;
|
|
65
|
+
const scopedFiles = filterByScope(context.files, row.scope);
|
|
66
|
+
if (scopedFiles.length === 0 && row.trigger_type !== "commit" && row.trigger_type !== "pre-commit")
|
|
67
|
+
continue;
|
|
68
|
+
const scopedCtx = {
|
|
69
|
+
...context,
|
|
70
|
+
files: scopedFiles
|
|
71
|
+
};
|
|
72
|
+
const result = checkFn(scopedCtx);
|
|
73
|
+
if (!result.passed) {
|
|
74
|
+
allViolations.push(...result.violations);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
passed: allViolations.length === 0,
|
|
79
|
+
violations: allViolations
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
RuleEngine
|
|
85
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
const SCHEMA = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS rules (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
name TEXT NOT NULL,
|
|
9
|
+
description TEXT NOT NULL DEFAULT '',
|
|
10
|
+
trigger_type TEXT NOT NULL DEFAULT 'on-demand',
|
|
11
|
+
severity TEXT NOT NULL DEFAULT 'warn',
|
|
12
|
+
scope TEXT NOT NULL DEFAULT '**/*',
|
|
13
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
14
|
+
builtin INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
created_at INTEGER NOT NULL,
|
|
16
|
+
updated_at INTEGER NOT NULL
|
|
17
|
+
)`;
|
|
18
|
+
class RuleStore {
|
|
19
|
+
constructor(db) {
|
|
20
|
+
this.db = db;
|
|
21
|
+
this.db.exec(SCHEMA);
|
|
22
|
+
}
|
|
23
|
+
upsert(rule) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
this.db.prepare(
|
|
26
|
+
`INSERT INTO rules (id, name, description, trigger_type, severity, scope, enabled, builtin, created_at, updated_at)
|
|
27
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
28
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
29
|
+
name = excluded.name,
|
|
30
|
+
description = excluded.description,
|
|
31
|
+
trigger_type = excluded.trigger_type,
|
|
32
|
+
severity = excluded.severity,
|
|
33
|
+
scope = excluded.scope,
|
|
34
|
+
enabled = excluded.enabled,
|
|
35
|
+
builtin = excluded.builtin,
|
|
36
|
+
updated_at = excluded.updated_at`
|
|
37
|
+
).run(
|
|
38
|
+
rule.id,
|
|
39
|
+
rule.name,
|
|
40
|
+
rule.description,
|
|
41
|
+
rule.trigger_type,
|
|
42
|
+
rule.severity,
|
|
43
|
+
rule.scope,
|
|
44
|
+
rule.enabled,
|
|
45
|
+
rule.builtin,
|
|
46
|
+
now,
|
|
47
|
+
now
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
getAll() {
|
|
51
|
+
return this.db.prepare("SELECT * FROM rules ORDER BY id").all();
|
|
52
|
+
}
|
|
53
|
+
getEnabled() {
|
|
54
|
+
return this.db.prepare("SELECT * FROM rules WHERE enabled = 1 ORDER BY id").all();
|
|
55
|
+
}
|
|
56
|
+
getByTrigger(trigger) {
|
|
57
|
+
return this.db.prepare(
|
|
58
|
+
"SELECT * FROM rules WHERE enabled = 1 AND trigger_type = ? ORDER BY id"
|
|
59
|
+
).all(trigger);
|
|
60
|
+
}
|
|
61
|
+
getById(id) {
|
|
62
|
+
return this.db.prepare("SELECT * FROM rules WHERE id = ?").get(id);
|
|
63
|
+
}
|
|
64
|
+
setEnabled(id, enabled) {
|
|
65
|
+
const result = this.db.prepare("UPDATE rules SET enabled = ?, updated_at = ? WHERE id = ?").run(enabled ? 1 : 0, Date.now(), id);
|
|
66
|
+
return result.changes > 0;
|
|
67
|
+
}
|
|
68
|
+
delete(id) {
|
|
69
|
+
const result = this.db.prepare("DELETE FROM rules WHERE id = ?").run(id);
|
|
70
|
+
return result.changes > 0;
|
|
71
|
+
}
|
|
72
|
+
seedBuiltins(rules) {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const stmt = this.db.prepare(
|
|
75
|
+
`INSERT OR IGNORE INTO rules (id, name, description, trigger_type, severity, scope, enabled, builtin, created_at, updated_at)
|
|
76
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
77
|
+
);
|
|
78
|
+
const tx = this.db.transaction(() => {
|
|
79
|
+
for (const rule of rules) {
|
|
80
|
+
stmt.run(
|
|
81
|
+
rule.id,
|
|
82
|
+
rule.name,
|
|
83
|
+
rule.description,
|
|
84
|
+
rule.trigger_type,
|
|
85
|
+
rule.severity,
|
|
86
|
+
rule.scope,
|
|
87
|
+
rule.enabled,
|
|
88
|
+
rule.builtin,
|
|
89
|
+
now,
|
|
90
|
+
now
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
tx();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
RuleStore
|
|
99
|
+
};
|
|
@@ -4,3 +4,24 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
export * from "./types.js";
|
|
6
6
|
export * from "./skill-storage.js";
|
|
7
|
+
export * from "./skill-matcher.js";
|
|
8
|
+
import {
|
|
9
|
+
SkillRegistry,
|
|
10
|
+
getSkillRegistry,
|
|
11
|
+
resetSkillRegistry
|
|
12
|
+
} from "./skill-registry.js";
|
|
13
|
+
import { getSkillRegistry as getSkillRegistry2 } from "./skill-registry.js";
|
|
14
|
+
import { matchPrompt as matchPromptFn } from "./skill-matcher.js";
|
|
15
|
+
function matchPromptFromRegistry(prompt) {
|
|
16
|
+
const registry = getSkillRegistry2();
|
|
17
|
+
const rules = registry.getAllRules();
|
|
18
|
+
const { config, scoring } = registry.getMatcherConfig();
|
|
19
|
+
const mappings = registry.getDirectoryMappings();
|
|
20
|
+
return matchPromptFn(prompt, rules, config, scoring, mappings);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
SkillRegistry,
|
|
24
|
+
getSkillRegistry,
|
|
25
|
+
matchPromptFromRegistry,
|
|
26
|
+
resetSkillRegistry
|
|
27
|
+
};
|