@gobing-ai/ts-rule-engine 0.3.14 → 0.3.17
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 +113 -4
- package/dist/engine.d.ts +14 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +50 -4
- package/dist/evaluators/file-utils.d.ts +14 -1
- package/dist/evaluators/file-utils.d.ts.map +1 -1
- package/dist/evaluators/file-utils.js +20 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/persistence/adapter.d.ts +53 -0
- package/dist/persistence/adapter.d.ts.map +1 -0
- package/dist/persistence/adapter.js +0 -0
- package/dist/persistence/db-adapter.d.ts +12 -0
- package/dist/persistence/db-adapter.d.ts.map +1 -0
- package/dist/persistence/db-adapter.js +33 -0
- package/dist/persistence/index.d.ts +5 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +3 -0
- package/dist/persistence/memory-adapter.d.ts +39 -0
- package/dist/persistence/memory-adapter.d.ts.map +1 -0
- package/dist/persistence/memory-adapter.js +69 -0
- package/dist/persistence/schema.d.ts +11 -0
- package/dist/persistence/schema.d.ts.map +1 -0
- package/dist/persistence/schema.js +53 -0
- package/package.json +5 -4
- package/src/engine.ts +67 -4
- package/src/evaluators/file-utils.ts +19 -3
- package/src/index.ts +1 -0
- package/src/persistence/adapter.ts +66 -0
- package/src/persistence/db-adapter.ts +93 -0
- package/src/persistence/index.ts +4 -0
- package/src/persistence/memory-adapter.ts +106 -0
- package/src/persistence/schema.ts +54 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory persistence adapter for unit tests.
|
|
3
|
+
* Stores rows in Maps so tests can assert on persisted state.
|
|
4
|
+
*/
|
|
5
|
+
export class MemoryRulePersistenceAdapter {
|
|
6
|
+
constructor() {
|
|
7
|
+
// Explicit constructor for V8 function coverage — class implements an interface, no super().
|
|
8
|
+
}
|
|
9
|
+
runs = new Map();
|
|
10
|
+
evals = new Map();
|
|
11
|
+
async insertRun(input) {
|
|
12
|
+
const now = new Date().toISOString();
|
|
13
|
+
this.runs.set(input.id, {
|
|
14
|
+
...input,
|
|
15
|
+
status: 'running',
|
|
16
|
+
findingCount: 0,
|
|
17
|
+
fixCount: 0,
|
|
18
|
+
appliedFixCount: 0,
|
|
19
|
+
durationMs: null,
|
|
20
|
+
completedAt: null,
|
|
21
|
+
createdAt: now,
|
|
22
|
+
updatedAt: now,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async updateRunStatus(runId, status, findingCount, fixCount, appliedFixCount, durationMs) {
|
|
26
|
+
const row = this.runs.get(runId);
|
|
27
|
+
if (!row)
|
|
28
|
+
throw new Error(`Run not found: ${runId}`);
|
|
29
|
+
row.status = status;
|
|
30
|
+
row.findingCount = findingCount;
|
|
31
|
+
row.fixCount = fixCount;
|
|
32
|
+
row.appliedFixCount = appliedFixCount;
|
|
33
|
+
row.durationMs = durationMs;
|
|
34
|
+
row.completedAt = new Date().toISOString();
|
|
35
|
+
row.updatedAt = new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
async insertEvalRun(input) {
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const key = `${input.runId}:${input.ruleId}`;
|
|
40
|
+
this.evals.set(key, {
|
|
41
|
+
...input,
|
|
42
|
+
status: 'running',
|
|
43
|
+
findingCount: 0,
|
|
44
|
+
fixCount: 0,
|
|
45
|
+
durationMs: null,
|
|
46
|
+
error: null,
|
|
47
|
+
findingsJson: null,
|
|
48
|
+
fixesJson: null,
|
|
49
|
+
completedAt: null,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async updateEvalRun(input) {
|
|
55
|
+
const key = `${input.runId}:${input.ruleId}`;
|
|
56
|
+
const row = this.evals.get(key);
|
|
57
|
+
if (!row)
|
|
58
|
+
throw new Error(`Eval run not found: ${key}`);
|
|
59
|
+
row.status = input.status;
|
|
60
|
+
row.findingCount = input.findingCount;
|
|
61
|
+
row.fixCount = input.fixCount;
|
|
62
|
+
row.durationMs = input.durationMs;
|
|
63
|
+
row.error = input.error ?? null;
|
|
64
|
+
row.findingsJson = input.findingsJson ?? null;
|
|
65
|
+
row.fixesJson = input.fixesJson ?? null;
|
|
66
|
+
row.completedAt = new Date().toISOString();
|
|
67
|
+
row.updatedAt = new Date().toISOString();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-engine persistence schema.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the workflow-engine persistence pattern: the engine owns the DDL and
|
|
5
|
+
* direct writes; consumers query the resulting tables through their own DAOs.
|
|
6
|
+
* `rule_runs` is the list/query unit; `rule_eval_runs` is the per-rule detail
|
|
7
|
+
* unit. Timestamps are TEXT (ISO 8601) for portability across SQLite drivers
|
|
8
|
+
* including D1.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RULE_ENGINE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS rule_runs (\n id TEXT PRIMARY KEY,\n preset TEXT,\n source_kind TEXT NOT NULL,\n source_value TEXT,\n status TEXT NOT NULL DEFAULT 'running',\n rule_count INTEGER NOT NULL DEFAULT 0,\n finding_count INTEGER NOT NULL DEFAULT 0,\n fix_count INTEGER NOT NULL DEFAULT 0,\n applied_fix_count INTEGER NOT NULL DEFAULT 0,\n fail_on TEXT,\n stop_on_first TEXT,\n fix_mode TEXT NOT NULL DEFAULT 'none',\n dry_run INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n duration_ms INTEGER,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS rule_eval_runs (\n id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL REFERENCES rule_runs(id),\n rule_id TEXT NOT NULL,\n severity TEXT NOT NULL,\n evaluator TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'running',\n finding_count INTEGER NOT NULL DEFAULT 0,\n fix_count INTEGER NOT NULL DEFAULT 0,\n duration_ms INTEGER,\n error TEXT,\n findings_json TEXT,\n fixes_json TEXT,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_rule_eval_runs_run_id ON rule_eval_runs (run_id);\n";
|
|
11
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/persistence/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,sBAAsB,4zCA2ClC,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-engine persistence schema.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the workflow-engine persistence pattern: the engine owns the DDL and
|
|
5
|
+
* direct writes; consumers query the resulting tables through their own DAOs.
|
|
6
|
+
* `rule_runs` is the list/query unit; `rule_eval_runs` is the per-rule detail
|
|
7
|
+
* unit. Timestamps are TEXT (ISO 8601) for portability across SQLite drivers
|
|
8
|
+
* including D1.
|
|
9
|
+
*/
|
|
10
|
+
export const RULE_ENGINE_SCHEMA_SQL = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS rule_runs (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
preset TEXT,
|
|
14
|
+
source_kind TEXT NOT NULL,
|
|
15
|
+
source_value TEXT,
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
17
|
+
rule_count INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
finding_count INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
fix_count INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
applied_fix_count INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
fail_on TEXT,
|
|
22
|
+
stop_on_first TEXT,
|
|
23
|
+
fix_mode TEXT NOT NULL DEFAULT 'none',
|
|
24
|
+
dry_run INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
started_at TEXT NOT NULL,
|
|
26
|
+
completed_at TEXT,
|
|
27
|
+
duration_ms INTEGER,
|
|
28
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
29
|
+
created_at TEXT NOT NULL,
|
|
30
|
+
updated_at TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS rule_eval_runs (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
run_id TEXT NOT NULL REFERENCES rule_runs(id),
|
|
36
|
+
rule_id TEXT NOT NULL,
|
|
37
|
+
severity TEXT NOT NULL,
|
|
38
|
+
evaluator TEXT NOT NULL,
|
|
39
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
40
|
+
finding_count INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
fix_count INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
duration_ms INTEGER,
|
|
43
|
+
error TEXT,
|
|
44
|
+
findings_json TEXT,
|
|
45
|
+
fixes_json TEXT,
|
|
46
|
+
started_at TEXT NOT NULL,
|
|
47
|
+
completed_at TEXT,
|
|
48
|
+
created_at TEXT NOT NULL,
|
|
49
|
+
updated_at TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_rule_eval_runs_run_id ON rule_eval_runs (run_id);
|
|
53
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-rule-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -49,9 +49,10 @@
|
|
|
49
49
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-rule-engine-v<version> && git push --tags' && exit 1"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@gobing-ai/ts-ai-runner": "^0.3.
|
|
53
|
-
"@gobing-ai/ts-infra": "^0.3.
|
|
54
|
-
"@gobing-ai/ts-runtime": "^0.3.
|
|
52
|
+
"@gobing-ai/ts-ai-runner": "^0.3.17",
|
|
53
|
+
"@gobing-ai/ts-infra": "^0.3.17",
|
|
54
|
+
"@gobing-ai/ts-runtime": "^0.3.17",
|
|
55
|
+
"@gobing-ai/ts-db": "^0.3.17",
|
|
55
56
|
"yaml": "^2.7.0",
|
|
56
57
|
"zod": "^4.1.0"
|
|
57
58
|
},
|
package/src/engine.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from './fixers/fixers';
|
|
12
12
|
import { registerBuiltins } from './host/builtins';
|
|
13
13
|
import { RuleEngineHost } from './host/rule-engine-host';
|
|
14
|
+
import type { RulePersistenceAdapter } from './persistence/adapter';
|
|
14
15
|
import type { ConstraintFinding, ConstraintRule, Fix, FixMode, RuleEngineResult, RuleEvaluator } from './types';
|
|
15
16
|
import { createFinding, SEVERITY_RANK } from './types';
|
|
16
17
|
|
|
@@ -24,6 +25,12 @@ export interface RuleEngineOptions {
|
|
|
24
25
|
events?: EventBus<RuleEngineEvents>;
|
|
25
26
|
/** Optional logger; defaults to the shared `rule-engine` category logger. */
|
|
26
27
|
logger?: Logger;
|
|
28
|
+
/** Optional persistence adapter for durable run/eval history. */
|
|
29
|
+
persistence?: RulePersistenceAdapter;
|
|
30
|
+
/** Caller-provided run ID; engine generates one if absent. */
|
|
31
|
+
runId?: string;
|
|
32
|
+
/** Caller metadata for the run row (preset, failOn, source info, etc.). */
|
|
33
|
+
runMeta?: Record<string, unknown>;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
/** Orchestrates enabled constraint rules through a typed evaluator host. */
|
|
@@ -33,6 +40,9 @@ export class RuleEngine {
|
|
|
33
40
|
|
|
34
41
|
private readonly events: EventBus<RuleEngineEvents> | undefined;
|
|
35
42
|
private readonly logger: Logger;
|
|
43
|
+
private readonly persistence: RulePersistenceAdapter | undefined;
|
|
44
|
+
private readonly runId: string;
|
|
45
|
+
private readonly runMeta: Record<string, unknown> | undefined;
|
|
36
46
|
|
|
37
47
|
constructor(options: RuleEngineOptions = {}) {
|
|
38
48
|
this.host = options.host ?? new RuleEngineHost();
|
|
@@ -40,6 +50,9 @@ export class RuleEngine {
|
|
|
40
50
|
registerBuiltinFixers(this.host, options.processExecutor);
|
|
41
51
|
this.events = options.events;
|
|
42
52
|
this.logger = options.logger ?? getLogger('rule-engine');
|
|
53
|
+
this.persistence = options.persistence;
|
|
54
|
+
this.runId = options.runId ?? crypto.randomUUID();
|
|
55
|
+
this.runMeta = options.runMeta;
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
/** Register or replace an evaluator. */
|
|
@@ -70,6 +83,10 @@ export class RuleEngine {
|
|
|
70
83
|
* provider by evaluator type and calls `createFixes`. The effective fix mode
|
|
71
84
|
* is the minimum of the rule's configured mode and `maxFixMode`.
|
|
72
85
|
*
|
|
86
|
+
* When `options.persistence` is set, this method writes a run row before
|
|
87
|
+
* evaluation and per-rule eval rows during the loop. A `done`/`failed`
|
|
88
|
+
* status is written after the loop completes.
|
|
89
|
+
*
|
|
73
90
|
* @param rules - Normalized rule definitions to evaluate.
|
|
74
91
|
* @param workdir - Working directory to scan.
|
|
75
92
|
* @param maxFixMode - Highest fix authority requested by the caller.
|
|
@@ -87,6 +104,21 @@ export class RuleEngine {
|
|
|
87
104
|
const enabledRules = rules.filter((r) => r.enabled !== false);
|
|
88
105
|
const runStartMs = Date.now();
|
|
89
106
|
|
|
107
|
+
// Insert the run row before any evaluation.
|
|
108
|
+
const runMeta = this.runMeta ?? {};
|
|
109
|
+
await this.persistence?.insertRun({
|
|
110
|
+
id: this.runId,
|
|
111
|
+
sourceKind: (runMeta.sourceKind as 'preset' | 'file') ?? 'preset',
|
|
112
|
+
sourceValue: runMeta.sourceValue as string | undefined,
|
|
113
|
+
preset: runMeta.preset as string | undefined,
|
|
114
|
+
ruleCount: enabledRules.length,
|
|
115
|
+
failOn: runMeta.failOn as string | undefined,
|
|
116
|
+
stopOnFirst: runMeta.stopOnFirst as string | undefined,
|
|
117
|
+
fixMode: maxFixMode,
|
|
118
|
+
dryRun: (runMeta.dryRun as boolean) ?? false,
|
|
119
|
+
metadataJson: runMeta.metadataJson as string | undefined,
|
|
120
|
+
});
|
|
121
|
+
|
|
90
122
|
return await traceAsync(
|
|
91
123
|
'rule.run',
|
|
92
124
|
async () => {
|
|
@@ -103,6 +135,15 @@ export class RuleEngine {
|
|
|
103
135
|
if (rule.enabled === false) continue;
|
|
104
136
|
index++;
|
|
105
137
|
|
|
138
|
+
// Persist eval start.
|
|
139
|
+
await this.persistence?.insertEvalRun({
|
|
140
|
+
id: `${this.runId}:${rule.id}`,
|
|
141
|
+
runId: this.runId,
|
|
142
|
+
ruleId: rule.id,
|
|
143
|
+
severity: rule.severity,
|
|
144
|
+
evaluator: rule.evaluator.type,
|
|
145
|
+
});
|
|
146
|
+
|
|
106
147
|
const evalStartMs = Date.now();
|
|
107
148
|
this.logger.debug('eval start', { ruleId: rule.id, index, total: enabledRules.length });
|
|
108
149
|
addSpanEvent('rule.eval.start', { ruleId: rule.id, index, total: enabledRules.length });
|
|
@@ -110,6 +151,7 @@ export class RuleEngine {
|
|
|
110
151
|
|
|
111
152
|
let ruleFindings: ConstraintFinding[] = [];
|
|
112
153
|
let ruleEvalFixes: Fix[] = [];
|
|
154
|
+
let evalError: string | undefined;
|
|
113
155
|
try {
|
|
114
156
|
const result = await this.host.evaluators
|
|
115
157
|
.get(rule.evaluator.type)
|
|
@@ -117,15 +159,15 @@ export class RuleEngine {
|
|
|
117
159
|
ruleFindings = result.findings;
|
|
118
160
|
ruleEvalFixes = result.fixes;
|
|
119
161
|
} catch (error) {
|
|
120
|
-
|
|
162
|
+
evalError = error instanceof Error ? error.message : String(error);
|
|
121
163
|
ruleFindings = [
|
|
122
|
-
createFinding(rule,
|
|
164
|
+
createFinding(rule, evalError, null, {
|
|
123
165
|
code: `evaluator:${rule.evaluator.type}`,
|
|
124
166
|
kind: 'error',
|
|
125
167
|
}),
|
|
126
168
|
];
|
|
127
|
-
addSpanEvent('rule.eval.error', { ruleId: rule.id, error:
|
|
128
|
-
void this.events?.emit('rule.eval.error', { ruleId: rule.id, error:
|
|
169
|
+
addSpanEvent('rule.eval.error', { ruleId: rule.id, error: evalError });
|
|
170
|
+
void this.events?.emit('rule.eval.error', { ruleId: rule.id, error: evalError });
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
const durationMs = Date.now() - evalStartMs;
|
|
@@ -140,6 +182,17 @@ export class RuleEngine {
|
|
|
140
182
|
durationMs,
|
|
141
183
|
});
|
|
142
184
|
|
|
185
|
+
// Persist eval completion.
|
|
186
|
+
await this.persistence?.updateEvalRun({
|
|
187
|
+
runId: this.runId,
|
|
188
|
+
ruleId: rule.id,
|
|
189
|
+
status: evalError ? 'failed' : 'done',
|
|
190
|
+
findingCount: ruleFindings.length,
|
|
191
|
+
fixCount: ruleEvalFixes.length,
|
|
192
|
+
durationMs,
|
|
193
|
+
...(evalError ? { error: evalError } : {}),
|
|
194
|
+
});
|
|
195
|
+
|
|
143
196
|
findings.push(...ruleFindings);
|
|
144
197
|
fixes.push(...ruleEvalFixes);
|
|
145
198
|
|
|
@@ -193,6 +246,16 @@ export class RuleEngine {
|
|
|
193
246
|
stoppedEarly: stoppedEarlyLocal,
|
|
194
247
|
});
|
|
195
248
|
|
|
249
|
+
// Finalize the run row.
|
|
250
|
+
await this.persistence?.updateRunStatus(
|
|
251
|
+
this.runId,
|
|
252
|
+
'done',
|
|
253
|
+
findings.length,
|
|
254
|
+
fixes.length,
|
|
255
|
+
0, // appliedFixCount — set by the caller after applying fixes
|
|
256
|
+
runDurationMs,
|
|
257
|
+
);
|
|
258
|
+
|
|
196
259
|
return { findings, fixes };
|
|
197
260
|
},
|
|
198
261
|
{ attributes: { 'rule.count': enabledRules.length } },
|
|
@@ -124,12 +124,28 @@ export function relativeParent(path: string): string {
|
|
|
124
124
|
return parent === '.' ? '' : parent;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Return true when a path matches any supplied pattern.
|
|
129
|
+
*
|
|
130
|
+
* Each pattern is matched in two passes, most-specific first:
|
|
131
|
+
* 1. If it contains a glob metachar (`*`), try anchored {@link matchesGlob} —
|
|
132
|
+
* so `packages/**\/*.ts` resolves segment-by-segment instead of collapsing
|
|
133
|
+
* to a bare fragment (the latter silently matched nothing, leaving every
|
|
134
|
+
* glob-scoped loose-mode rule scanning zero files).
|
|
135
|
+
* 2. Otherwise (or if the glob did not match), fall back to the legacy
|
|
136
|
+
* fragment/suffix substring test, where stars are stripped — preserving
|
|
137
|
+
* bare fragments like `.ts`, `src/`, `/tests/` that loose-mode evaluators
|
|
138
|
+
* have always accepted, and keeping unanchored patterns like `*.ts`
|
|
139
|
+
* (→ suffix `.ts`) matching nested paths.
|
|
140
|
+
*/
|
|
128
141
|
export function matchesAny(path: string, patterns: string[] | undefined): boolean {
|
|
129
142
|
if (patterns === undefined || patterns.length === 0) return true;
|
|
143
|
+
const normalizedPath = path.replaceAll('\\', '/');
|
|
130
144
|
return patterns.some((pattern) => {
|
|
131
|
-
const
|
|
132
|
-
|
|
145
|
+
const normalized = pattern.replaceAll('\\', '/');
|
|
146
|
+
if (normalized.includes('*') && matchesGlob(normalizedPath, normalized)) return true;
|
|
147
|
+
const clean = normalized.replaceAll('**/', '').replaceAll('*', '');
|
|
148
|
+
return clean.length === 0 || normalizedPath.includes(clean) || normalizedPath.endsWith(clean);
|
|
133
149
|
});
|
|
134
150
|
}
|
|
135
151
|
|
package/src/index.ts
CHANGED
|
@@ -15,5 +15,6 @@ export * from './formatters/json';
|
|
|
15
15
|
export * from './formatters/text';
|
|
16
16
|
export * from './host/bundled-rules';
|
|
17
17
|
export * from './host/rule-engine-host';
|
|
18
|
+
export * from './persistence';
|
|
18
19
|
export * from './resolvers/test-path-resolver';
|
|
19
20
|
export * from './types';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { FixMode } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Input for inserting a rule run row (engine caller provides run metadata). */
|
|
4
|
+
export interface RuleRunInput {
|
|
5
|
+
id: string;
|
|
6
|
+
preset?: string;
|
|
7
|
+
sourceKind: 'preset' | 'file';
|
|
8
|
+
sourceValue?: string;
|
|
9
|
+
ruleCount: number;
|
|
10
|
+
failOn?: string;
|
|
11
|
+
stopOnFirst?: string;
|
|
12
|
+
fixMode: FixMode;
|
|
13
|
+
dryRun: boolean;
|
|
14
|
+
metadataJson?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Input for inserting a per-rule eval row. */
|
|
18
|
+
export interface RuleEvalRunInput {
|
|
19
|
+
id: string;
|
|
20
|
+
runId: string;
|
|
21
|
+
ruleId: string;
|
|
22
|
+
severity: string;
|
|
23
|
+
evaluator: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Input for updating a per-rule eval row after evaluation completes. */
|
|
27
|
+
export interface RuleEvalRunUpdate {
|
|
28
|
+
runId: string;
|
|
29
|
+
ruleId: string;
|
|
30
|
+
status: 'done' | 'failed' | 'skipped';
|
|
31
|
+
findingCount: number;
|
|
32
|
+
fixCount: number;
|
|
33
|
+
durationMs: number;
|
|
34
|
+
/** Evaluator error message, if status is 'failed'. */
|
|
35
|
+
error?: string;
|
|
36
|
+
/** Bounded JSON for findings. */
|
|
37
|
+
findingsJson?: string;
|
|
38
|
+
/** Bounded JSON for fixes. */
|
|
39
|
+
fixesJson?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Persistence adapter contract for the rule engine.
|
|
44
|
+
* Implementations write directly to a durable store; the engine does NOT
|
|
45
|
+
* use EventBus subscribers for durability.
|
|
46
|
+
*/
|
|
47
|
+
export interface RulePersistenceAdapter {
|
|
48
|
+
/** Insert a run row as 'running'. Must be idempotent. */
|
|
49
|
+
insertRun(input: RuleRunInput): Promise<void>;
|
|
50
|
+
|
|
51
|
+
/** Finalize a run row after evaluation completes. */
|
|
52
|
+
updateRunStatus(
|
|
53
|
+
runId: string,
|
|
54
|
+
status: 'done' | 'failed',
|
|
55
|
+
findingCount: number,
|
|
56
|
+
fixCount: number,
|
|
57
|
+
appliedFixCount: number,
|
|
58
|
+
durationMs: number,
|
|
59
|
+
): Promise<void>;
|
|
60
|
+
|
|
61
|
+
/** Insert a per-rule eval row as 'running'. Must be idempotent. */
|
|
62
|
+
insertEvalRun(input: RuleEvalRunInput): Promise<void>;
|
|
63
|
+
|
|
64
|
+
/** Finalize a per-rule eval row after evaluation completes. */
|
|
65
|
+
updateEvalRun(input: RuleEvalRunUpdate): Promise<void>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { DbAdapter } from '@gobing-ai/ts-db';
|
|
2
|
+
|
|
3
|
+
import type { RuleEvalRunInput, RuleEvalRunUpdate, RulePersistenceAdapter, RuleRunInput } from './adapter';
|
|
4
|
+
|
|
5
|
+
/** Persist engine run/eval rows to SQLite via a ts-db DbAdapter. */
|
|
6
|
+
export class DbRulePersistenceAdapter implements RulePersistenceAdapter {
|
|
7
|
+
constructor(private readonly db: DbAdapter) {}
|
|
8
|
+
|
|
9
|
+
async insertRun(input: RuleRunInput): Promise<void> {
|
|
10
|
+
const now = new Date().toISOString();
|
|
11
|
+
await this.db.run(
|
|
12
|
+
`INSERT INTO rule_runs (id, preset, source_kind, source_value, status, rule_count,
|
|
13
|
+
fail_on, stop_on_first, fix_mode, dry_run, metadata_json, started_at, created_at, updated_at)
|
|
14
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
15
|
+
input.id,
|
|
16
|
+
input.preset ?? null,
|
|
17
|
+
input.sourceKind,
|
|
18
|
+
input.sourceValue ?? null,
|
|
19
|
+
input.ruleCount,
|
|
20
|
+
input.failOn ?? null,
|
|
21
|
+
input.stopOnFirst ?? null,
|
|
22
|
+
input.fixMode,
|
|
23
|
+
input.dryRun ? 1 : 0,
|
|
24
|
+
input.metadataJson ?? '{}',
|
|
25
|
+
now,
|
|
26
|
+
now,
|
|
27
|
+
now,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async updateRunStatus(
|
|
32
|
+
runId: string,
|
|
33
|
+
status: 'done' | 'failed',
|
|
34
|
+
findingCount: number,
|
|
35
|
+
fixCount: number,
|
|
36
|
+
appliedFixCount: number,
|
|
37
|
+
durationMs: number,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
await this.db.run(
|
|
41
|
+
`UPDATE rule_runs
|
|
42
|
+
SET status = ?, finding_count = ?, fix_count = ?, applied_fix_count = ?,
|
|
43
|
+
duration_ms = ?, completed_at = ?, updated_at = ?
|
|
44
|
+
WHERE id = ?`,
|
|
45
|
+
status,
|
|
46
|
+
findingCount,
|
|
47
|
+
fixCount,
|
|
48
|
+
appliedFixCount,
|
|
49
|
+
durationMs,
|
|
50
|
+
now,
|
|
51
|
+
now,
|
|
52
|
+
runId,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async insertEvalRun(input: RuleEvalRunInput): Promise<void> {
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
await this.db.run(
|
|
59
|
+
`INSERT INTO rule_eval_runs (id, run_id, rule_id, severity, evaluator, status,
|
|
60
|
+
started_at, created_at, updated_at)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, 'running', ?, ?, ?)`,
|
|
62
|
+
input.id,
|
|
63
|
+
input.runId,
|
|
64
|
+
input.ruleId,
|
|
65
|
+
input.severity,
|
|
66
|
+
input.evaluator,
|
|
67
|
+
now,
|
|
68
|
+
now,
|
|
69
|
+
now,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async updateEvalRun(input: RuleEvalRunUpdate): Promise<void> {
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
await this.db.run(
|
|
76
|
+
`UPDATE rule_eval_runs
|
|
77
|
+
SET status = ?, finding_count = ?, fix_count = ?, duration_ms = ?,
|
|
78
|
+
error = ?, findings_json = ?, fixes_json = ?, completed_at = ?, updated_at = ?
|
|
79
|
+
WHERE run_id = ? AND rule_id = ?`,
|
|
80
|
+
input.status,
|
|
81
|
+
input.findingCount,
|
|
82
|
+
input.fixCount,
|
|
83
|
+
input.durationMs,
|
|
84
|
+
input.error ?? null,
|
|
85
|
+
input.findingsJson ?? null,
|
|
86
|
+
input.fixesJson ?? null,
|
|
87
|
+
now,
|
|
88
|
+
now,
|
|
89
|
+
input.runId,
|
|
90
|
+
input.ruleId,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { RuleEvalRunInput, RuleEvalRunUpdate, RulePersistenceAdapter, RuleRunInput } from './adapter';
|
|
2
|
+
export { DbRulePersistenceAdapter } from './db-adapter';
|
|
3
|
+
export { MemoryRulePersistenceAdapter } from './memory-adapter';
|
|
4
|
+
export { RULE_ENGINE_SCHEMA_SQL } from './schema';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { RuleEvalRunInput, RuleEvalRunUpdate, RulePersistenceAdapter, RuleRunInput } from './adapter';
|
|
2
|
+
|
|
3
|
+
/** Runtime row shape for the memory adapter. */
|
|
4
|
+
interface RuleRunRow extends RuleRunInput {
|
|
5
|
+
status: 'running' | 'done' | 'failed';
|
|
6
|
+
findingCount: number;
|
|
7
|
+
fixCount: number;
|
|
8
|
+
appliedFixCount: number;
|
|
9
|
+
durationMs: number | null;
|
|
10
|
+
completedAt: string | null;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RuleEvalRow extends RuleEvalRunInput {
|
|
16
|
+
status: 'running' | 'done' | 'failed' | 'skipped';
|
|
17
|
+
findingCount: number;
|
|
18
|
+
fixCount: number;
|
|
19
|
+
durationMs: number | null;
|
|
20
|
+
error: string | null;
|
|
21
|
+
findingsJson: string | null;
|
|
22
|
+
fixesJson: string | null;
|
|
23
|
+
completedAt: string | null;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* In-memory persistence adapter for unit tests.
|
|
30
|
+
* Stores rows in Maps so tests can assert on persisted state.
|
|
31
|
+
*/
|
|
32
|
+
export class MemoryRulePersistenceAdapter implements RulePersistenceAdapter {
|
|
33
|
+
constructor() {
|
|
34
|
+
// Explicit constructor for V8 function coverage — class implements an interface, no super().
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
readonly runs = new Map<string, RuleRunRow>();
|
|
38
|
+
readonly evals = new Map<string, RuleEvalRow>();
|
|
39
|
+
|
|
40
|
+
async insertRun(input: RuleRunInput): Promise<void> {
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
this.runs.set(input.id, {
|
|
43
|
+
...input,
|
|
44
|
+
status: 'running',
|
|
45
|
+
findingCount: 0,
|
|
46
|
+
fixCount: 0,
|
|
47
|
+
appliedFixCount: 0,
|
|
48
|
+
durationMs: null,
|
|
49
|
+
completedAt: null,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async updateRunStatus(
|
|
56
|
+
runId: string,
|
|
57
|
+
status: 'done' | 'failed',
|
|
58
|
+
findingCount: number,
|
|
59
|
+
fixCount: number,
|
|
60
|
+
appliedFixCount: number,
|
|
61
|
+
durationMs: number,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const row = this.runs.get(runId);
|
|
64
|
+
if (!row) throw new Error(`Run not found: ${runId}`);
|
|
65
|
+
row.status = status;
|
|
66
|
+
row.findingCount = findingCount;
|
|
67
|
+
row.fixCount = fixCount;
|
|
68
|
+
row.appliedFixCount = appliedFixCount;
|
|
69
|
+
row.durationMs = durationMs;
|
|
70
|
+
row.completedAt = new Date().toISOString();
|
|
71
|
+
row.updatedAt = new Date().toISOString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async insertEvalRun(input: RuleEvalRunInput): Promise<void> {
|
|
75
|
+
const now = new Date().toISOString();
|
|
76
|
+
const key = `${input.runId}:${input.ruleId}`;
|
|
77
|
+
this.evals.set(key, {
|
|
78
|
+
...input,
|
|
79
|
+
status: 'running',
|
|
80
|
+
findingCount: 0,
|
|
81
|
+
fixCount: 0,
|
|
82
|
+
durationMs: null,
|
|
83
|
+
error: null,
|
|
84
|
+
findingsJson: null,
|
|
85
|
+
fixesJson: null,
|
|
86
|
+
completedAt: null,
|
|
87
|
+
createdAt: now,
|
|
88
|
+
updatedAt: now,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async updateEvalRun(input: RuleEvalRunUpdate): Promise<void> {
|
|
93
|
+
const key = `${input.runId}:${input.ruleId}`;
|
|
94
|
+
const row = this.evals.get(key);
|
|
95
|
+
if (!row) throw new Error(`Eval run not found: ${key}`);
|
|
96
|
+
row.status = input.status;
|
|
97
|
+
row.findingCount = input.findingCount;
|
|
98
|
+
row.fixCount = input.fixCount;
|
|
99
|
+
row.durationMs = input.durationMs;
|
|
100
|
+
row.error = input.error ?? null;
|
|
101
|
+
row.findingsJson = input.findingsJson ?? null;
|
|
102
|
+
row.fixesJson = input.fixesJson ?? null;
|
|
103
|
+
row.completedAt = new Date().toISOString();
|
|
104
|
+
row.updatedAt = new Date().toISOString();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-engine persistence schema.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the workflow-engine persistence pattern: the engine owns the DDL and
|
|
5
|
+
* direct writes; consumers query the resulting tables through their own DAOs.
|
|
6
|
+
* `rule_runs` is the list/query unit; `rule_eval_runs` is the per-rule detail
|
|
7
|
+
* unit. Timestamps are TEXT (ISO 8601) for portability across SQLite drivers
|
|
8
|
+
* including D1.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const RULE_ENGINE_SCHEMA_SQL = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS rule_runs (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
preset TEXT,
|
|
15
|
+
source_kind TEXT NOT NULL,
|
|
16
|
+
source_value TEXT,
|
|
17
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
18
|
+
rule_count INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
finding_count INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
fix_count INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
applied_fix_count INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
fail_on TEXT,
|
|
23
|
+
stop_on_first TEXT,
|
|
24
|
+
fix_mode TEXT NOT NULL DEFAULT 'none',
|
|
25
|
+
dry_run INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
started_at TEXT NOT NULL,
|
|
27
|
+
completed_at TEXT,
|
|
28
|
+
duration_ms INTEGER,
|
|
29
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
30
|
+
created_at TEXT NOT NULL,
|
|
31
|
+
updated_at TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS rule_eval_runs (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
run_id TEXT NOT NULL REFERENCES rule_runs(id),
|
|
37
|
+
rule_id TEXT NOT NULL,
|
|
38
|
+
severity TEXT NOT NULL,
|
|
39
|
+
evaluator TEXT NOT NULL,
|
|
40
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
41
|
+
finding_count INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
fix_count INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
duration_ms INTEGER,
|
|
44
|
+
error TEXT,
|
|
45
|
+
findings_json TEXT,
|
|
46
|
+
fixes_json TEXT,
|
|
47
|
+
started_at TEXT NOT NULL,
|
|
48
|
+
completed_at TEXT,
|
|
49
|
+
created_at TEXT NOT NULL,
|
|
50
|
+
updated_at TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_rule_eval_runs_run_id ON rule_eval_runs (run_id);
|
|
54
|
+
`;
|