Rule Engine Validation Result Truncation Bug Fix (Late 2026-07-11 / Early 07-12)
Issue Source
A previous review uncovered an anomaly: For the same item_type='Project', validation using selected_ids (real objects) returned 9 items with blocked=False, but using directly passed empty Project data returned 4 items with blocked=True, and all 4 items had matched=False — a contradiction (blocked but unable to see which rule caused the block).
Two Actual Bugs
Bug 1: When block returns early, the blocking rule is not merged into results (incomplete results)
Location: The execute() loop in server/core/rule-engine.js.
Original Logic:
if (rule.action_type === 'block' && isBlocked) {
return { executed: true, blocked: true, block_reason: actionResult.message, results, ... };
}
results 此时只含之前已评估且 matched=false 的规则,触发拦截的当前规则 resultItem 未 push。
Consequence: HTTP returns blocked=True, but the blocking rule with matched=true is not visible in the results; block_reason points to a rule that does not exist in the results → creating the paradox of 'all false yet blocked'.
Fix:
- Before returning, results.push(resultItem);
- Added a blocking_rule: rule.id field, allowing the frontend to clearly identify which rule caused the block.
Bug 2: The validate function's default first_match strategy hides subsequent, more severe block violations
The default conflict_strategy for execute is 'first_match'. After sorting rules by priority, P1(warn) rules are placed before P0(block) rules (because priority||2 sets P0=0 to 2, making P1=1 smaller). If a warn rule is matched=true first, first_match breaks immediately, causing subsequent mandatory validation rules at the block level (e.g., project-validate-001/003) to never be evaluated → hard violations are missed in validation results.
Consequence: Mandatory field omissions that should be highlighted in red in the validation report are silently discarded.
Fix: The default strategy is now differentiated by scope:
- validate / inspect → merge (evaluate all rules, collect all violations);
- others (repair/optimize, etc.) → retain first_match (apply the first automatic fix upon match, aligning with original intent).
The block branch still serves as a termination condition (return upon encountering the first block), ensuring determinism.
Verification (direct engine testing + HTTP end-to-end)
- Empty Project (data-only): Now blocked=True, blocking_rule=builtin-project-validate-001, and this rule appears in results with matched=True, resolving the paradox.
- Real Project (selected_ids): blocked=False, 9 results, only generic rules triggered and passed=True (not blocked), correct.
- Full capability regression: validate Part (6 items, not blocked)/repair Part (1 item written back)/optimize (4 items)/inspect (618 items) all normal, no regression.
Additional confirmation (not a bug).
- builtin-project-validate-003 requires scheduling_method to be non-empty: the scheduling_method in the real SCSAI Project is a valid ID reference, a truly mandatory field; the previous 'OK Project' test data missed scheduling_method, triggering the block—engine behavior is correct.
- The discrepancy between HTTP validate and direct engine test result sets was originally thought to be 'missing project rules,' but was actually caused by Bug2's first_match breaking early; now unified.
Modified files.
- server/core/rule-engine.js: execute() in two places (block returns push resultItem + blocking_rule; default conflict_strategy scoped).
MemoryService recordTrace fails silently (discovered and fixed in the same cycle)
Issue
server.err logs 'MemoryService recordTrace failed: Cannot read properties of null (reading 'prepare')' for every request.
capability-dispatcher calls memoryService.extractAndStoreFacts in a fire-and-forget manner; its failure is swallowed by a try/catch,
resulting in long-term silent loss of all memory writes (trace/fact/profile) and no observable signals beyond any effective alerts.
Root Cause
- dbAdapter.init() is asynchronous and does not flip any synchronous switch (dbAdapter._initialized remains undefined).
The old _getDb returns null when _initialized is falsey.
- sqliteCompat.createDatabase returns a 'deferred proxy' when sql.js is not ready; the proxy's prepare throws an error if _realDb is not populated.
The old _getDb synchronously called prepare(...).run() and threw immediately, but the error was swallowed → this._db remained null forever.
- recordTrace was originally synchronous, and extractAndStoreFacts did not await it (it was turned into a Promise first),
causing subsequent db.prepare to crash when db was still null.
Fix (server/services/memory-service.js):
- _getDb() changed to return only the ready instance (returns null if not ready, no longer swallows errors to create a DB).
- Added new async _ensureDb(): after awaiting dbAdapter.init(), call dbAdapter.createDatabaseFresh(DB_PATH) (same as the rule engine, available within server), then _initTables().
- recordTrace changed to async, awaits this._ensureDb() if no db exists.
- extractAndStoreFacts changed to async and awaits recordTrace; safely returns if db is missing.
- Preheat memoryService._ensureDb() before module export to make read interfaces (getProfile/getFacts/getTraces) ready.
Validation
- After restart, initiate a validate request. The memory_traces table in boss_analytics.db is automatically created and one record is written
(source=web, action=validate), and the recordTrace error no longer appears in server.err.
Summary of modified files
- server/core/rule-engine.js: Added rule_id in two places in execute() and in executeOptimize.
- server/services/memory-service.js: Modified _getDb/_ensureDb/recordTrace/extractAndStoreFacts + module preheating.
- server/core/health-inspector.js: Changed _getDb to use createDatabaseFresh (to avoid failure in table creation due to delayed proxy returned by createDatabase lacking .run()).
Incidental fixes / confirmations
1. optimize response missing rule_id
executeOptimize's optimizations.push only includes rule: rule.name, missing rule_id → frontend/audit cannot see which rule was applied.
Added rule_id: rule.id in server/core/rule-engine.js; restart verified return includes rule_id=builtin-optimize-00x.
2. HealthInspector table creation failed (this._db.run is not a function).
health-inspector.js uses compat.createDatabase(dbPath), which returns a 'delayed proxy' (only prepare/exec, no run) when sql.js is not ready,
causing _ensureTables' CREATE TABLE ... .run() to throw error, and health check to fail silently. Changed to compat.createDatabaseFresh(dbPath) (real instance,
same as rule engine/memory service). After restart, server.err no longer shows table creation failure / save result failure.
3. Other observations (not bugs).
- identify initially returned empty: due to test parameter misalignment (passed query instead of query_type+filters). Changed to query_type=list, then source=rule_engine returned normally.
- [RuleEngine] SCSAI private library query failed: Unknown error: Only cold‑start one‑time SCSAI query failed, not systemic.
- Configuration module failed to load, using default configuration: Cannot find module './cron‑tasks': Configuration fallback warning, harmless.
4. Six‑capability end‑to‑end stability verification (real Part, after restart)
validate / inspect / repair(auto_apply) / optimize(auto_apply real write‑back to SCSAI) / identify / generate all success=OK, no runtime errors.
optimize auto_apply=true actually wrote back to SCSAI successfully (SCSAI_response.success=true, name/description rewritten).
Status
- All repairs completed, server restart verified: six capabilities stable, memory writes restored, health‑check table built normally, no recordTrace/create‑table/prepare errors.
- No residual standalone bugs.
BossAgents