Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .aiox-core/core/ideation/ideation-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ class IdeationEngine {
let filtered = suggestions;
if (this.gotchasMemory) {
try {
const knownIssues = await this.gotchasMemory.getAll();
const knownIssues = await this.gotchasMemory.listGotchas();
filtered = suggestions.filter((s) => !this.isKnownGotcha(s, knownIssues));
} catch {
// Ignore
} catch (error) {
console.warn('[IdeationEngine] Gotchas filtering failed:', error.message);
}
}

Expand Down
40 changes: 36 additions & 4 deletions .aiox-core/core/memory/gotchas-memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class GotchasMemory extends EventEmitter {

// In-memory storage
this.gotchas = new Map(); // id -> gotcha
this.errorTracking = new Map(); // errorHash -> { count, firstSeen, lastSeen, samples }
this.errorTracking = new Map(); // errorHash -> { count, firstSeen, lastSeen, timestamps, samples }

// Load existing data
this._loadGotchas();
Expand Down Expand Up @@ -253,15 +253,27 @@ class GotchasMemory extends EventEmitter {
count: 0,
firstSeen: now,
lastSeen: now,
timestamps: [],
samples: [],
errorPattern: errorData.message,
category: this._detectCategory(errorData.message + ' ' + (errorData.stack || '')),
};
}

// Update tracking
tracking.count++;
// Keep only occurrences inside the configured rolling error window.
const windowStart = now - this.options.errorWindowMs;
const timestamps = this._normalizeErrorTimestamps(tracking).filter((timestamp) => timestamp >= windowStart);
timestamps.push(now);

tracking.timestamps = timestamps;
tracking.count = timestamps.length;
tracking.firstSeen = timestamps[0];
tracking.lastSeen = now;
tracking.samples = (tracking.samples || []).filter((sample) => {
const timestamp = Date.parse(sample.timestamp);
return Number.isFinite(timestamp) && timestamp >= windowStart;
});

if (tracking.samples.length < 5) {
tracking.samples.push({
timestamp: new Date(now).toISOString(),
Expand Down Expand Up @@ -314,7 +326,7 @@ class GotchasMemory extends EventEmitter {
// Sort by severity (critical first), then by last occurrence
const severityOrder = { critical: 0, warning: 1, info: 2 };
gotchas.sort((a, b) => {
const severityDiff = (severityOrder[a.severity] || 2) - (severityOrder[b.severity] || 2);
const severityDiff = (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2);
if (severityDiff !== 0) return severityDiff;
return new Date(b.source.lastSeen) - new Date(a.source.lastSeen);
});
Expand Down Expand Up @@ -712,6 +724,26 @@ class GotchasMemory extends EventEmitter {
return null;
}

/**
* Normalize persisted error occurrence timestamps.
* @private
* @param {Object} tracking - Error tracking entry
* @returns {number[]} Timestamp list in milliseconds
*/
_normalizeErrorTimestamps(tracking) {
const timestamps = Array.isArray(tracking.timestamps) ? tracking.timestamps : [tracking.lastSeen];

return timestamps
.map((timestamp) => {
if (typeof timestamp === 'number') {
return timestamp;
}
const parsed = Date.parse(timestamp);
return Number.isFinite(parsed) ? parsed : null;
})
.filter((timestamp) => timestamp !== null);
}

/**
* Generate title from error message
* @private
Expand Down
57 changes: 55 additions & 2 deletions .aiox-core/core/orchestration/condition-evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@
* @property {string} reason - Reason for the decision
*/

const RUNTIME_CONDITION_DEFAULTS = Object.freeze({
after_prd_creation: true,
architecture_changes_needed: true,
architecture_suggests_prd_changes: true,
based_on_classification: true,
documentation_inadequate: true,
epic_complete: true,
gate_approved: true,
major_enhancement_path: true,
po_checklist_issues: true,
qa_left_unchecked_items: true,
stories_remaining: true,
user_has_generated_ui: true,
user_wants_ai_generation: true,
user_wants_story_review: true,
});

/**
* Evaluates workflow conditions based on detected tech stack
*/
Expand All @@ -36,6 +53,7 @@ class ConditionEvaluator {
// Context for QA approval tracking (updated externally)
this._qaApproved = false;
this._phaseOutputs = {};
this._runtimeOverrides = new Map();
}

/**
Expand All @@ -54,6 +72,25 @@ class ConditionEvaluator {
this._phaseOutputs = outputs;
}

/**
* Set a runtime-resolved condition value.
* @param {string} name - Condition name
* @param {boolean} value - Runtime condition value
*/
setRuntimeCondition(name, value) {
this._runtimeOverrides.set(name, Boolean(value));
}

/**
* Set multiple runtime-resolved condition values.
* @param {Object} overrides - Map of condition names to values
*/
setRuntimeConditions(overrides = {}) {
for (const [name, value] of Object.entries(overrides)) {
this.setRuntimeCondition(name, value);
}
}

/**
* Evaluate a condition string
* @param {string} condition - Condition like 'project_has_database'
Expand All @@ -65,6 +102,10 @@ class ConditionEvaluator {
return true;
}

if (this._runtimeOverrides.has(condition)) {
return this._runtimeOverrides.get(condition);
}

// Built-in condition evaluators
const evaluators = {
// Tech stack conditions
Expand Down Expand Up @@ -105,6 +146,10 @@ class ConditionEvaluator {
return evaluator();
}

if (Object.prototype.hasOwnProperty.call(RUNTIME_CONDITION_DEFAULTS, condition)) {
return RUNTIME_CONDITION_DEFAULTS[condition];
}

// Handle negation first
if (condition.startsWith('!')) {
return !this.evaluate(condition.substring(1).trim());
Expand Down Expand Up @@ -145,9 +190,9 @@ class ConditionEvaluator {
return this._evaluateDotNotation(condition);
}

// Unknown condition - default to true (permissive)
// Unknown condition - fail safe so typos do not silently authorize execution.
console.warn(`[ConditionEvaluator] Unknown condition: ${condition}`);
return true;
return false;
}

/**
Expand Down Expand Up @@ -336,6 +381,14 @@ class ConditionEvaluator {
project_has_backend: 'No backend framework detected',
supabase_configured: 'Supabase not configured or missing environment variables',
qa_review_approved: 'QA review not yet approved',
user_wants_ai_generation: 'User did not request AI generation',
architecture_suggests_prd_changes: 'Architecture did not request PRD changes',
po_checklist_issues: 'PO checklist has no blocking issues',
user_has_generated_ui: 'No generated UI artifact was provided',
user_wants_story_review: 'User did not request story review',
qa_left_unchecked_items: 'QA did not leave unchecked items',
stories_remaining: 'No stories remaining',
epic_complete: 'Epic is not complete',
};

const reasons = failed.map((c) => explanations[c] || `Condition not met: ${c}`);
Expand Down
16 changes: 8 additions & 8 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# - SHA256 hashes for change detection
# - File types for categorization
#
version: 5.1.9
generated_at: "2026-05-07T15:10:49.402Z"
version: 5.1.13
generated_at: "2026-05-07T16:42:59.200Z"
generator: scripts/generate-install-manifest.js
file_count: 1103
files:
Expand Down Expand Up @@ -693,9 +693,9 @@ files:
type: core
size: 7755
- path: core/ideation/ideation-engine.js
hash: sha256:a154a0ce92f6b39b358529a030025b9709e0e4e2b85f6af02bd474e15c1aeb83
hash: sha256:c91f7bc999eca74394eeae9bc7400e63e134de4092d6bdc08b92bf423e423ec3
type: core
size: 23808
size: 23886
- path: core/ids/circuit-breaker.js
hash: sha256:63be126f2e0d320daa60cb5b68b21df93cad4c19d1f959e06a6ac776213f8eba
type: core
Expand Down Expand Up @@ -793,9 +793,9 @@ files:
type: core
size: 8850
- path: core/memory/gotchas-memory.js
hash: sha256:0063eff42caf0dda759c0390ac323e7b102f5507f27b8beb7b0acc2fbec407c4
hash: sha256:deeb90a6dff9ef284537a1dabf52566cec3f64244f31f4081432515985a94e25
type: core
size: 33058
size: 34271
- path: core/migration/migration-config.yaml
hash: sha256:c251213b99ce86c9b15311d51a0b464b7fc25179455c4c6c107cae76cf49d1ab
type: core
Expand Down Expand Up @@ -833,9 +833,9 @@ files:
type: core
size: 19293
- path: core/orchestration/condition-evaluator.js
hash: sha256:8bf565cf56194340ff4e1d642647150775277bce649411d0338faa2c96106745
hash: sha256:daa6755c76359bb99a2e687c9c56f74b52b7151b0b0677a771b8fff24538d2ad
type: core
size: 10845
size: 12722
- path: core/orchestration/context-manager.js
hash: sha256:7bf273723a2c08cb84e670b9d4c55aacec51819b1fbd5f3b0c46c4ccfa2ec192
type: core
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# STORY-123.18: Corrigir cluster runtime de resiliência

## Status

Done

## Story

Como mantenedor do AIOX Core, quero fechar o cluster de bugs de resiliência dos PRs antigos #584, #470, #474 e #477, para que ideação, condições de workflow, circuit breaker e memória de gotchas operem com comportamento fail-safe e sem regressões silenciosas.

## Acceptance Criteria

- [x] AC1. `IdeationEngine` usa a API existente `listGotchas()` e registra falhas de filtro com contexto.
- [x] AC2. `ConditionEvaluator` retorna `false` para condições desconhecidas e preserva condições runtime conhecidas com overrides explícitos.
- [x] AC3. `CircuitBreaker` preserva o timeout de recuperação quando novas falhas chegam no estado `OPEN`.
- [x] AC4. `GotchasMemory` aplica `errorWindowMs` como janela de contagem e ordena severidade `critical` antes de `warning` e `info`.
- [x] AC5. Testes regressivos cobrem o cluster e o manifest do core é regenerado.

## Tasks

- [x] Confrontar PRs antigos contra `main`.
- [x] Implementar apenas os resíduos ainda presentes no runtime.
- [x] Adicionar regressões focadas para o cluster.
- [x] Atualizar versão e manifest de instalação.
- [x] Rodar validações locais antes de PR.

## Dev Notes

- #470 já estava implementado no `main`, mas ganhou teste regressivo para permitir fechar o PR antigo com evidência.
- #517 já tinha o import nomeado corrigido em PR anterior, mas o fluxo ainda chamava `getAll()`, API inexistente em `GotchasMemory`.
- #584 continua dirty e carrega `entity-registry`/manifest antigo; esta story substitui o PR por uma correção atual e mínima.

## Validation

- `node -c .aiox-core/core/ideation/ideation-engine.js` -> PASS.
- `node -c .aiox-core/core/orchestration/condition-evaluator.js` -> PASS.
- `node -c .aiox-core/core/memory/gotchas-memory.js` -> PASS.
- `node -c tests/core/resilience-regressions.test.js` -> PASS.
- `npm test -- tests/core/resilience-regressions.test.js --runInBand --forceExit` -> PASS, 1 suite / 6 tests.
- `git diff --check` -> PASS.
- `npm run validate:manifest` -> PASS.
- `npm run validate:semantic-lint` -> PASS.
- `npm run validate:publish` -> PASS.
- `npm run lint -- --quiet` -> PASS.
- `npm run typecheck` -> PASS.
- `npm test -- tests/core/resilience-regressions.test.js tests/core/gotchas-memory-imports.test.js --runInBand --forceExit` -> PASS, 2 suites / 11 tests.
- `npm run test:ci` -> PASS, 316 suites / 7.853 tests.

## File List

- `.aiox-core/core/ideation/ideation-engine.js`
- `.aiox-core/core/orchestration/condition-evaluator.js`
- `.aiox-core/core/memory/gotchas-memory.js`
- `.aiox-core/install-manifest.yaml`
- `tests/core/resilience-regressions.test.js`
- `package.json`
- `package-lock.json`
- `docs/stories/epic-123/STORY-123.18-resilience-runtime-cluster.md`
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aiox-squads/core",
"version": "5.1.12",
"version": "5.1.13",
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
"bin": {
"aiox": "bin/aiox.js",
Expand Down
Loading
Loading