@gobing-ai/ts-rule-engine 0.3.14 → 0.3.16

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 CHANGED
@@ -31,7 +31,7 @@ erDiagram
31
31
 
32
32
  | Entity | One-line Description |
33
33
  |--------|----------------------|
34
- | `RuleEngine` | Orchestrates evaluation of enabled rules against a workspace directory. Supports opt-in early exit via `stopOnFirst` parameter. Accepts an optional `EventBus<RuleEngineEvents>` for structured in-process observability. |
34
+ | `RuleEngine` | Orchestrates evaluation of enabled rules against a workspace directory. Constructor accepts `RuleEngineOptions` (host, persistence, events, runId, runMeta, etc.). Supports opt-in early exit via `stopOnFirst` parameter. |
35
35
  | `RuleEngineHost` | Capability host backed by `CapabilityRegistry` from `@gobing-ai/ts-runtime/extension` — holds evaluators, resolvers, formatters, and fixer providers, each with origin tracking for safe override detection. |
36
36
  | `CapabilityRegistry<T>` | Generic named registry (shared with `ts-rule-engine`, `ts-dual-workflow-engine`) that tags each entry with its `origin` (`'builtin'`, `'extension'`, `'caller'`). |
37
37
  | `ConstraintRule` | Declarative policy unit: id, severity, include/exclude globs, evaluator type + config, and optional fix config. |
@@ -45,6 +45,8 @@ erDiagram
45
45
  | `Fix` | Candidate byte-range replacement; collected separately from findings, written only on explicit `applyFixes()`. |
46
46
  | `RuleEngineResult` | Aggregate `{ findings, fixes }` returned from a single evaluation run. |
47
47
  | `RuleEngineEvents` | Typed event map for rule-engine observability. All events prefixed `rule.` — see [Observability](#observability). |
48
+ | `RulePersistenceAdapter` | Durable adapter contract. Engine calls `insertRun`/`updateRunStatus` for the run, `insertEvalRun`/`updateEvalRun` per rule. DB-backed and memory-backed implementations included. |
49
+ | `RULE_ENGINE_SCHEMA_SQL` | Engine-owned DDL export: `rule_runs` + `rule_eval_runs` tables plus the FK index. Consumers apply it up-front before creating a `DbRulePersistenceAdapter`. |
48
50
  | `bundledRulesRoot()` | Resolves the absolute path to the bundled `rules/` directory shipped with this package — portable defaults usable as the lowest-priority preset root. |
49
51
  ## Mental Model
50
52
 
@@ -65,11 +67,12 @@ flowchart TD
65
67
  Core concepts:
66
68
 
67
69
  - `ConstraintRule`: one policy check. It has an `id`, `severity`, evaluator type/config, optional include/exclude globs, and optional fix config.
68
- - `RuleEngine`: runs enabled rules against a `workdir`. The constructor auto-registers built-in evaluators, formatters, resolvers, and fixer providers.
70
+ - `RuleEngine`: runs enabled rules against a `workdir`. The constructor auto-registers built-in evaluators, formatters, resolvers, and fixer providers. Accepts `RuleEngineOptions` for a custom host, `EventBus`, `processExecutor`, `persistence` adapter, `runId`, and `runMeta`.
69
71
  - `RuleEngineHost`: capability container backed by four `CapabilityRegistry` instances from `@gobing-ai/ts-runtime/extension`. Each entry tracks its origin (`'builtin'` / `'extension'` / `'caller'`) so the engine can detect and report conflicting registrations.
70
72
  - `RuleEvaluator`: implementation of one rule type, such as `regex`, `path`, or `coverage-gate`.
71
73
  - `Fix`: byte-range replacement candidate. Fixes are collected separately from findings and only written when you call `applyFixes()`.
72
74
  - Preset: YAML/JSON file that composes rule categories and can expose extension modules.
75
+ - `RulePersistenceAdapter`: durable run/eval history. The engine writes directly to the adapter at lifecycle boundaries; a DB-backed adapter and a test-friendly memory adapter ship with the package.
73
76
  - `bundledRulesRoot()`: resolves the path to the `rules/` directory shipped with this package. Pass it as the lowest-priority root to `loadPreset()` so project-local and user-global roots shadow individual files while inheriting the rest.
74
77
 
75
78
  ## Execution Flow
@@ -819,15 +822,121 @@ When no `events` option is provided, the engine incurs zero observability overhe
819
822
 
820
823
  `rule.eval.error` is emitted when an evaluator **throws** — it signals a crash, not a policy violation. The engine still produces a `kind: 'error'` finding for the thrown rule. A normal policy violation emits **no** `rule.eval.error`. Don't conflate the two: subscribe to `rule.eval.error` for crash alerting, and inspect findings for policy results.
821
824
 
825
+ ## Persistence
826
+
827
+ The engine supports a **durable persistence adapter** pattern (mirrors `@gobing-ai/ts-dual-workflow-engine`). When a `RulePersistenceAdapter` is injected via `RuleEngineOptions`, the engine writes run and per-rule evaluation rows directly — no EventBus subscriber needed.
828
+
829
+ ### DB Schema
830
+
831
+ ```mermaid
832
+ erDiagram
833
+ RULE_RUNS ||--o{ RULE_EVAL_RUNS : "run_id → id"
834
+ RULE_RUNS {
835
+ TEXT id PK
836
+ TEXT preset
837
+ TEXT source_kind "preset | file"
838
+ TEXT source_value
839
+ TEXT status "running | done | failed"
840
+ INTEGER rule_count
841
+ INTEGER finding_count
842
+ INTEGER fix_count
843
+ INTEGER applied_fix_count
844
+ TEXT fail_on
845
+ TEXT stop_on_first
846
+ TEXT fix_mode "none | suggest | auto"
847
+ INTEGER dry_run
848
+ TEXT started_at "ISO 8601"
849
+ TEXT completed_at "ISO 8601"
850
+ INTEGER duration_ms
851
+ TEXT metadata_json "{}"
852
+ TEXT created_at "ISO 8601"
853
+ TEXT updated_at "ISO 8601"
854
+ }
855
+ RULE_EVAL_RUNS {
856
+ TEXT id PK
857
+ TEXT run_id FK
858
+ TEXT rule_id
859
+ TEXT severity "error | warning | info"
860
+ TEXT evaluator
861
+ TEXT status "running | done | failed | skipped"
862
+ INTEGER finding_count
863
+ INTEGER fix_count
864
+ INTEGER duration_ms
865
+ TEXT error
866
+ TEXT findings_json
867
+ TEXT fixes_json
868
+ TEXT started_at "ISO 8601"
869
+ TEXT completed_at "ISO 8601"
870
+ TEXT created_at "ISO 8601"
871
+ TEXT updated_at "ISO 8601"
872
+ }
873
+ ```
874
+
875
+ - **`rule_runs`** is the list/query unit — one row per engine invocation.
876
+ - **`rule_eval_runs`** is the per-rule detail unit — one row per evaluated rule, FK'd to `rule_runs`.
877
+ - Timestamps are `TEXT` (ISO 8601) for portability across SQLite drivers including D1.
878
+ - `status`: `running` during evaluation, `done` on normal completion (including policy failures), `failed` for engine/runtime errors. `skipped` when `stopOnFirst` prevents later rules from running.
879
+
880
+ ### Adapter Contract
881
+
882
+ ```ts
883
+ interface RulePersistenceAdapter {
884
+ insertRun(input: RuleRunInput): Promise<void>;
885
+ updateRunStatus(runId, status, findingCount, fixCount, appliedFixCount, durationMs): Promise<void>;
886
+ insertEvalRun(input: RuleEvalRunInput): Promise<void>;
887
+ updateEvalRun(input: RuleEvalRunUpdate): Promise<void>;
888
+ }
889
+ ```
890
+
891
+ Two implementations ship with the package:
892
+
893
+ | Adapter | What it does |
894
+ |---------|-------------|
895
+ | `DbRulePersistenceAdapter` | Writes directly to SQLite via a `@gobing-ai/ts-db` `DbAdapter`. Consumers apply the schema up-front with `RULE_ENGINE_SCHEMA_SQL`. |
896
+ | `MemoryRulePersistenceAdapter` | Stores rows in `Map<string, Row>` — designed for unit tests. Lets test code assert on persisted run/eval state. |
897
+
898
+ ### Usage
899
+
900
+ ```ts
901
+ import { RuleEngine, DbRulePersistenceAdapter, RULE_ENGINE_SCHEMA_SQL } from '@gobing-ai/ts-rule-engine';
902
+ import { createDbAdapter } from '@gobing-ai/ts-db';
903
+
904
+ const db = await createDbAdapter({ driver: 'bun-sqlite', url: ':memory:' });
905
+ await db.exec(RULE_ENGINE_SCHEMA_SQL);
906
+
907
+ const engine = new RuleEngine({
908
+ persistence: new DbRulePersistenceAdapter(db),
909
+ runId: 'my-run-1',
910
+ runMeta: {
911
+ sourceKind: 'preset',
912
+ sourceValue: 'recommended',
913
+ preset: 'recommended',
914
+ failOn: 'error',
915
+ stopOnFirst: 'false',
916
+ fixMode: 'none',
917
+ dryRun: false,
918
+ },
919
+ });
920
+
921
+ const result = await engine.evaluate(rules, process.cwd());
922
+ // At this point, rule_runs has 'my-run-1' with status='done', findings, and duration.
923
+ ```
924
+
925
+ - `runId`: caller-provided or engine-generated (`crypto.randomUUID()`). Persisted so downstream tools like `spur rule trace` can query history.
926
+ - `runMeta`: metadata written to the initial run row — preset name, source/file info, fix mode, dry-run flag.
927
+ - Persistence is **incremental**: the run row is inserted before the first rule, finalized after the last. Per-rule eval rows are inserted as each rule starts and updated when it finishes. A polling `trace` can surface partial progress.
928
+ - When `persistence` is absent, the engine works normally — zero persistence overhead.
929
+
822
930
  ## Package Boundary
823
931
 
824
- This package owns rule definitions, preset loading, evaluators, formatters, test-path resolvers, fixer providers, extension loading, the `CapabilityRegistry` re-export from `@gobing-ai/ts-runtime/extension`, and bundled rule presets.
932
+ This package owns rule definitions, preset loading, evaluators, formatters, test-path resolvers, fixer providers, extension loading, the `CapabilityRegistry` re-export from `@gobing-ai/ts-runtime/extension`, the persistence adapter contract and implementations, the engine-owned schema SQL, and bundled rule presets.
825
933
 
826
934
  It does **not** own:
827
935
 
828
936
  - CLI argument parsing or process exit policy
829
937
  - `ProcessExecutor` implementation (injected via `RuleEngineOptions`)
938
+ - `DbAdapter` creation and lifecycle (injected via `RulePersistenceAdapter`)
830
939
  - Repository-specific rule catalogs (project rules live in `.spur/rules/`)
831
940
  - CI integration or publishing workflows
832
941
 
833
- Downstream tools (e.g. `spur`) consume the library and add their own CLI, config discovery, and execution policy.
942
+ Downstream tools (e.g. `spur`) consume the library and add their own CLI, config discovery, run-id stamping, and execution policy.
package/dist/engine.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { ProcessExecutor } from '@gobing-ai/ts-runtime';
3
3
  import type { RuleEngineEvents } from './events';
4
4
  import { type FixApplicationResult } from './fixers/fixers';
5
5
  import { RuleEngineHost } from './host/rule-engine-host';
6
+ import type { RulePersistenceAdapter } from './persistence/adapter';
6
7
  import type { ConstraintRule, Fix, FixMode, RuleEngineResult, RuleEvaluator } from './types';
7
8
  /** Options for constructing a RuleEngine. */
8
9
  export interface RuleEngineOptions {
@@ -14,6 +15,12 @@ export interface RuleEngineOptions {
14
15
  events?: EventBus<RuleEngineEvents>;
15
16
  /** Optional logger; defaults to the shared `rule-engine` category logger. */
16
17
  logger?: Logger;
18
+ /** Optional persistence adapter for durable run/eval history. */
19
+ persistence?: RulePersistenceAdapter;
20
+ /** Caller-provided run ID; engine generates one if absent. */
21
+ runId?: string;
22
+ /** Caller metadata for the run row (preset, failOn, source info, etc.). */
23
+ runMeta?: Record<string, unknown>;
17
24
  }
18
25
  /** Orchestrates enabled constraint rules through a typed evaluator host. */
19
26
  export declare class RuleEngine {
@@ -21,6 +28,9 @@ export declare class RuleEngine {
21
28
  readonly host: RuleEngineHost;
22
29
  private readonly events;
23
30
  private readonly logger;
31
+ private readonly persistence;
32
+ private readonly runId;
33
+ private readonly runMeta;
24
34
  constructor(options?: RuleEngineOptions);
25
35
  /** Register or replace an evaluator. */
26
36
  registerEvaluator(type: string, evaluator: RuleEvaluator): void;
@@ -40,6 +50,10 @@ export declare class RuleEngine {
40
50
  * provider by evaluator type and calls `createFixes`. The effective fix mode
41
51
  * is the minimum of the rule's configured mode and `maxFixMode`.
42
52
  *
53
+ * When `options.persistence` is set, this method writes a run row before
54
+ * evaluation and per-rule eval rows during the loop. A `done`/`failed`
55
+ * status is written after the loop completes.
56
+ *
43
57
  * @param rules - Normalized rule definitions to evaluate.
44
58
  * @param workdir - Working directory to scan.
45
59
  * @param maxFixMode - Highest fix authority requested by the caller.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAIH,KAAK,oBAAoB,EAE5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAqB,cAAc,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGhH,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAC9B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,mCAAmC;IACnC,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACpC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,4EAA4E;AAC5E,qBAAa,UAAU;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAE9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,GAAE,iBAAsB;IAQ3C,wCAAwC;IACxC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAI/D;;;;;;;OAOG;IACG,QAAQ,CACV,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAI5B;;;;;;;;;;;;;;OAcG;IACG,iBAAiB,CACnB,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,OAAgB,EAC5B,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAoH5B;;;;;;;OAOG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,EAAE,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAG1G"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAIH,KAAK,oBAAoB,EAE5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAqB,cAAc,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGhH,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAC9B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,mCAAmC;IACnC,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACpC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,4EAA4E;AAC5E,qBAAa,UAAU;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAE9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;gBAElD,OAAO,GAAE,iBAAsB;IAW3C,wCAAwC;IACxC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAI/D;;;;;;;OAOG;IACG,QAAQ,CACV,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAI5B;;;;;;;;;;;;;;;;;;OAkBG;IACG,iBAAiB,CACnB,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,OAAgB,EAC5B,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAkK5B;;;;;;;OAOG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,EAAE,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAG1G"}
package/dist/engine.js CHANGED
@@ -9,12 +9,18 @@ export class RuleEngine {
9
9
  host;
10
10
  events;
11
11
  logger;
12
+ persistence;
13
+ runId;
14
+ runMeta;
12
15
  constructor(options = {}) {
13
16
  this.host = options.host ?? new RuleEngineHost();
14
17
  registerBuiltins(this.host, options.processExecutor);
15
18
  registerBuiltinFixers(this.host, options.processExecutor);
16
19
  this.events = options.events;
17
20
  this.logger = options.logger ?? getLogger('rule-engine');
21
+ this.persistence = options.persistence;
22
+ this.runId = options.runId ?? crypto.randomUUID();
23
+ this.runMeta = options.runMeta;
18
24
  }
19
25
  /** Register or replace an evaluator. */
20
26
  registerEvaluator(type, evaluator) {
@@ -38,6 +44,10 @@ export class RuleEngine {
38
44
  * provider by evaluator type and calls `createFixes`. The effective fix mode
39
45
  * is the minimum of the rule's configured mode and `maxFixMode`.
40
46
  *
47
+ * When `options.persistence` is set, this method writes a run row before
48
+ * evaluation and per-rule eval rows during the loop. A `done`/`failed`
49
+ * status is written after the loop completes.
50
+ *
41
51
  * @param rules - Normalized rule definitions to evaluate.
42
52
  * @param workdir - Working directory to scan.
43
53
  * @param maxFixMode - Highest fix authority requested by the caller.
@@ -49,6 +59,20 @@ export class RuleEngine {
49
59
  async evaluateWithFixes(rules, workdir, maxFixMode = 'auto', stopOnFirst) {
50
60
  const enabledRules = rules.filter((r) => r.enabled !== false);
51
61
  const runStartMs = Date.now();
62
+ // Insert the run row before any evaluation.
63
+ const runMeta = this.runMeta ?? {};
64
+ await this.persistence?.insertRun({
65
+ id: this.runId,
66
+ sourceKind: runMeta.sourceKind ?? 'preset',
67
+ sourceValue: runMeta.sourceValue,
68
+ preset: runMeta.preset,
69
+ ruleCount: enabledRules.length,
70
+ failOn: runMeta.failOn,
71
+ stopOnFirst: runMeta.stopOnFirst,
72
+ fixMode: maxFixMode,
73
+ dryRun: runMeta.dryRun ?? false,
74
+ metadataJson: runMeta.metadataJson,
75
+ });
52
76
  return await traceAsync('rule.run', async () => {
53
77
  this.logger.info('rule run started', { enabled: enabledRules.length, total: rules.length });
54
78
  addSpanEvent('rule.run.start', { rules: enabledRules.length, total: rules.length });
@@ -61,12 +85,21 @@ export class RuleEngine {
61
85
  if (rule.enabled === false)
62
86
  continue;
63
87
  index++;
88
+ // Persist eval start.
89
+ await this.persistence?.insertEvalRun({
90
+ id: `${this.runId}:${rule.id}`,
91
+ runId: this.runId,
92
+ ruleId: rule.id,
93
+ severity: rule.severity,
94
+ evaluator: rule.evaluator.type,
95
+ });
64
96
  const evalStartMs = Date.now();
65
97
  this.logger.debug('eval start', { ruleId: rule.id, index, total: enabledRules.length });
66
98
  addSpanEvent('rule.eval.start', { ruleId: rule.id, index, total: enabledRules.length });
67
99
  void this.events?.emit('rule.eval.start', { ruleId: rule.id, index, total: enabledRules.length });
68
100
  let ruleFindings = [];
69
101
  let ruleEvalFixes = [];
102
+ let evalError;
70
103
  try {
71
104
  const result = await this.host.evaluators
72
105
  .get(rule.evaluator.type)
@@ -75,15 +108,15 @@ export class RuleEngine {
75
108
  ruleEvalFixes = result.fixes;
76
109
  }
77
110
  catch (error) {
78
- const message = error instanceof Error ? error.message : String(error);
111
+ evalError = error instanceof Error ? error.message : String(error);
79
112
  ruleFindings = [
80
- createFinding(rule, message, null, {
113
+ createFinding(rule, evalError, null, {
81
114
  code: `evaluator:${rule.evaluator.type}`,
82
115
  kind: 'error',
83
116
  }),
84
117
  ];
85
- addSpanEvent('rule.eval.error', { ruleId: rule.id, error: message });
86
- void this.events?.emit('rule.eval.error', { ruleId: rule.id, error: message });
118
+ addSpanEvent('rule.eval.error', { ruleId: rule.id, error: evalError });
119
+ void this.events?.emit('rule.eval.error', { ruleId: rule.id, error: evalError });
87
120
  }
88
121
  const durationMs = Date.now() - evalStartMs;
89
122
  addSpanEvent('rule.eval.done', {
@@ -96,6 +129,16 @@ export class RuleEngine {
96
129
  findings: ruleFindings.length,
97
130
  durationMs,
98
131
  });
132
+ // Persist eval completion.
133
+ await this.persistence?.updateEvalRun({
134
+ runId: this.runId,
135
+ ruleId: rule.id,
136
+ status: evalError ? 'failed' : 'done',
137
+ findingCount: ruleFindings.length,
138
+ fixCount: ruleEvalFixes.length,
139
+ durationMs,
140
+ ...(evalError ? { error: evalError } : {}),
141
+ });
99
142
  findings.push(...ruleFindings);
100
143
  fixes.push(...ruleEvalFixes);
101
144
  const ruleMode = rule.fix?.mode ?? 'none';
@@ -142,6 +185,9 @@ export class RuleEngine {
142
185
  durationMs: runDurationMs,
143
186
  stoppedEarly: stoppedEarlyLocal,
144
187
  });
188
+ // Finalize the run row.
189
+ await this.persistence?.updateRunStatus(this.runId, 'done', findings.length, fixes.length, 0, // appliedFixCount — set by the caller after applying fixes
190
+ runDurationMs);
145
191
  return { findings, fixes };
146
192
  }, { attributes: { 'rule.count': enabledRules.length } });
147
193
  }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './formatters/json';
10
10
  export * from './formatters/text';
11
11
  export * from './host/bundled-rules';
12
12
  export * from './host/rule-engine-host';
13
+ export * from './persistence';
13
14
  export * from './resolvers/test-path-resolver';
14
15
  export * from './types';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAClH,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAClH,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC;AAC9B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -14,5 +14,6 @@ export * from './formatters/json.js';
14
14
  export * from './formatters/text.js';
15
15
  export * from './host/bundled-rules.js';
16
16
  export * from './host/rule-engine-host.js';
17
+ export * from './persistence/index.js';
17
18
  export * from './resolvers/test-path-resolver.js';
18
19
  export * from './types.js';
@@ -0,0 +1,53 @@
1
+ import type { FixMode } from '../types';
2
+ /** Input for inserting a rule run row (engine caller provides run metadata). */
3
+ export interface RuleRunInput {
4
+ id: string;
5
+ preset?: string;
6
+ sourceKind: 'preset' | 'file';
7
+ sourceValue?: string;
8
+ ruleCount: number;
9
+ failOn?: string;
10
+ stopOnFirst?: string;
11
+ fixMode: FixMode;
12
+ dryRun: boolean;
13
+ metadataJson?: string;
14
+ }
15
+ /** Input for inserting a per-rule eval row. */
16
+ export interface RuleEvalRunInput {
17
+ id: string;
18
+ runId: string;
19
+ ruleId: string;
20
+ severity: string;
21
+ evaluator: string;
22
+ }
23
+ /** Input for updating a per-rule eval row after evaluation completes. */
24
+ export interface RuleEvalRunUpdate {
25
+ runId: string;
26
+ ruleId: string;
27
+ status: 'done' | 'failed' | 'skipped';
28
+ findingCount: number;
29
+ fixCount: number;
30
+ durationMs: number;
31
+ /** Evaluator error message, if status is 'failed'. */
32
+ error?: string;
33
+ /** Bounded JSON for findings. */
34
+ findingsJson?: string;
35
+ /** Bounded JSON for fixes. */
36
+ fixesJson?: string;
37
+ }
38
+ /**
39
+ * Persistence adapter contract for the rule engine.
40
+ * Implementations write directly to a durable store; the engine does NOT
41
+ * use EventBus subscribers for durability.
42
+ */
43
+ export interface RulePersistenceAdapter {
44
+ /** Insert a run row as 'running'. Must be idempotent. */
45
+ insertRun(input: RuleRunInput): Promise<void>;
46
+ /** Finalize a run row after evaluation completes. */
47
+ updateRunStatus(runId: string, status: 'done' | 'failed', findingCount: number, fixCount: number, appliedFixCount: number, durationMs: number): Promise<void>;
48
+ /** Insert a per-rule eval row as 'running'. Must be idempotent. */
49
+ insertEvalRun(input: RuleEvalRunInput): Promise<void>;
50
+ /** Finalize a per-rule eval row after evaluation completes. */
51
+ updateEvalRun(input: RuleEvalRunUpdate): Promise<void>;
52
+ }
53
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/persistence/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,gFAAgF;AAChF,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACnC,yDAAyD;IACzD,SAAS,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C,qDAAqD;IACrD,eAAe,CACX,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,QAAQ,EACzB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB,mEAAmE;IACnE,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtD,+DAA+D;IAC/D,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1D"}
File without changes
@@ -0,0 +1,12 @@
1
+ import type { DbAdapter } from '@gobing-ai/ts-db';
2
+ import type { RuleEvalRunInput, RuleEvalRunUpdate, RulePersistenceAdapter, RuleRunInput } from './adapter';
3
+ /** Persist engine run/eval rows to SQLite via a ts-db DbAdapter. */
4
+ export declare class DbRulePersistenceAdapter implements RulePersistenceAdapter {
5
+ private readonly db;
6
+ constructor(db: DbAdapter);
7
+ insertRun(input: RuleRunInput): Promise<void>;
8
+ updateRunStatus(runId: string, status: 'done' | 'failed', findingCount: number, fixCount: number, appliedFixCount: number, durationMs: number): Promise<void>;
9
+ insertEvalRun(input: RuleEvalRunInput): Promise<void>;
10
+ updateEvalRun(input: RuleEvalRunUpdate): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=db-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-adapter.d.ts","sourceRoot":"","sources":["../../src/persistence/db-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE3G,oEAAoE;AACpE,qBAAa,wBAAyB,YAAW,sBAAsB;IACvD,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,SAAS;IAEpC,SAAS,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB7C,eAAe,CACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,QAAQ,EACzB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAkBV,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBrD,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;CAoB/D"}
@@ -0,0 +1,33 @@
1
+ /** Persist engine run/eval rows to SQLite via a ts-db DbAdapter. */
2
+ export class DbRulePersistenceAdapter {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ async insertRun(input) {
8
+ const now = new Date().toISOString();
9
+ await this.db.run(`INSERT INTO rule_runs (id, preset, source_kind, source_value, status, rule_count,
10
+ fail_on, stop_on_first, fix_mode, dry_run, metadata_json, started_at, created_at, updated_at)
11
+ VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, ?, ?, ?, ?, ?)`, input.id, input.preset ?? null, input.sourceKind, input.sourceValue ?? null, input.ruleCount, input.failOn ?? null, input.stopOnFirst ?? null, input.fixMode, input.dryRun ? 1 : 0, input.metadataJson ?? '{}', now, now, now);
12
+ }
13
+ async updateRunStatus(runId, status, findingCount, fixCount, appliedFixCount, durationMs) {
14
+ const now = new Date().toISOString();
15
+ await this.db.run(`UPDATE rule_runs
16
+ SET status = ?, finding_count = ?, fix_count = ?, applied_fix_count = ?,
17
+ duration_ms = ?, completed_at = ?, updated_at = ?
18
+ WHERE id = ?`, status, findingCount, fixCount, appliedFixCount, durationMs, now, now, runId);
19
+ }
20
+ async insertEvalRun(input) {
21
+ const now = new Date().toISOString();
22
+ await this.db.run(`INSERT INTO rule_eval_runs (id, run_id, rule_id, severity, evaluator, status,
23
+ started_at, created_at, updated_at)
24
+ VALUES (?, ?, ?, ?, ?, 'running', ?, ?, ?)`, input.id, input.runId, input.ruleId, input.severity, input.evaluator, now, now, now);
25
+ }
26
+ async updateEvalRun(input) {
27
+ const now = new Date().toISOString();
28
+ await this.db.run(`UPDATE rule_eval_runs
29
+ SET status = ?, finding_count = ?, fix_count = ?, duration_ms = ?,
30
+ error = ?, findings_json = ?, fixes_json = ?, completed_at = ?, updated_at = ?
31
+ WHERE run_id = ? AND rule_id = ?`, input.status, input.findingCount, input.fixCount, input.durationMs, input.error ?? null, input.findingsJson ?? null, input.fixesJson ?? null, now, now, input.runId, input.ruleId);
32
+ }
33
+ }
@@ -0,0 +1,5 @@
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';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/persistence/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC3G,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { DbRulePersistenceAdapter } from './db-adapter.js';
2
+ export { MemoryRulePersistenceAdapter } from './memory-adapter.js';
3
+ export { RULE_ENGINE_SCHEMA_SQL } from './schema.js';
@@ -0,0 +1,39 @@
1
+ import type { RuleEvalRunInput, RuleEvalRunUpdate, RulePersistenceAdapter, RuleRunInput } from './adapter';
2
+ /** Runtime row shape for the memory adapter. */
3
+ interface RuleRunRow extends RuleRunInput {
4
+ status: 'running' | 'done' | 'failed';
5
+ findingCount: number;
6
+ fixCount: number;
7
+ appliedFixCount: number;
8
+ durationMs: number | null;
9
+ completedAt: string | null;
10
+ createdAt: string;
11
+ updatedAt: string;
12
+ }
13
+ interface RuleEvalRow extends RuleEvalRunInput {
14
+ status: 'running' | 'done' | 'failed' | 'skipped';
15
+ findingCount: number;
16
+ fixCount: number;
17
+ durationMs: number | null;
18
+ error: string | null;
19
+ findingsJson: string | null;
20
+ fixesJson: string | null;
21
+ completedAt: string | null;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+ /**
26
+ * In-memory persistence adapter for unit tests.
27
+ * Stores rows in Maps so tests can assert on persisted state.
28
+ */
29
+ export declare class MemoryRulePersistenceAdapter implements RulePersistenceAdapter {
30
+ constructor();
31
+ readonly runs: Map<string, RuleRunRow>;
32
+ readonly evals: Map<string, RuleEvalRow>;
33
+ insertRun(input: RuleRunInput): Promise<void>;
34
+ updateRunStatus(runId: string, status: 'done' | 'failed', findingCount: number, fixCount: number, appliedFixCount: number, durationMs: number): Promise<void>;
35
+ insertEvalRun(input: RuleEvalRunInput): Promise<void>;
36
+ updateEvalRun(input: RuleEvalRunUpdate): Promise<void>;
37
+ }
38
+ export {};
39
+ //# sourceMappingURL=memory-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-adapter.d.ts","sourceRoot":"","sources":["../../src/persistence/memory-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE3G,gDAAgD;AAChD,UAAU,UAAW,SAAQ,YAAY;IACrC,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,WAAY,SAAQ,gBAAgB;IAC1C,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,4BAA6B,YAAW,sBAAsB;;IAKvE,QAAQ,CAAC,IAAI,0BAAiC;IAC9C,QAAQ,CAAC,KAAK,2BAAkC;IAE1C,SAAS,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAe7C,eAAe,CACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,QAAQ,EACzB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAYV,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBrD,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;CAc/D"}
@@ -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.14",
3
+ "version": "0.3.16",
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.14",
53
- "@gobing-ai/ts-infra": "^0.3.14",
54
- "@gobing-ai/ts-runtime": "^0.3.14",
52
+ "@gobing-ai/ts-ai-runner": "^0.3.16",
53
+ "@gobing-ai/ts-infra": "^0.3.16",
54
+ "@gobing-ai/ts-runtime": "^0.3.16",
55
+ "@gobing-ai/ts-db": "^0.3.16",
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
- const message = error instanceof Error ? error.message : String(error);
162
+ evalError = error instanceof Error ? error.message : String(error);
121
163
  ruleFindings = [
122
- createFinding(rule, message, null, {
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: message });
128
- void this.events?.emit('rule.eval.error', { ruleId: rule.id, error: message });
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 } },
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
+ `;