diff --git a/.aiox-core/core/execution/build-state-manager.js b/.aiox-core/core/execution/build-state-manager.js index 13744c5c86..6ba27d9bde 100644 --- a/.aiox-core/core/execution/build-state-manager.js +++ b/.aiox-core/core/execution/build-state-manager.js @@ -23,6 +23,11 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +const { + normalizeError, + sanitizeValue: sanitizeErrorValue, + serializeError, +} = require('../errors'); // Optional dependencies with graceful fallback let chalk; @@ -95,95 +100,9 @@ const NotificationType = { * @returns {*} Data-only representation that JSON.stringify can serialize. */ function sanitizeLogValue(value, seen = new WeakSet()) { - if (value === undefined || value === null) { - return value; - } - - const valueType = typeof value; - - if (valueType === 'bigint') { - return value.toString(); - } - - if (valueType === 'function' || valueType === 'symbol') { - return String(value); - } - - if (valueType !== 'object') { - return value; - } - - if (seen.has(value)) { - return '[Circular]'; - } - - if (value instanceof Date) { - return Number.isNaN(value.getTime()) ? value.toString() : value.toISOString(); - } - - if (value instanceof RegExp) { - return value.toString(); - } - - if (value instanceof Error) { - const safeError = { - name: value.name, - message: value.message, - stack: shouldExposeLogErrorStack() ? value.stack : '[redacted]', - }; - - seen.add(value); - - try { - Object.getOwnPropertyNames(value).forEach((key) => { - if (key === 'name' || key === 'message' || key === 'stack') { - return; - } - - try { - safeError[key] = sanitizeLogValue(value[key], seen); - } catch (error) { - safeError[key] = `[Unserializable: ${error.message}]`; - } - }); - - return safeError; - } finally { - seen.delete(value); - } - } - - seen.add(value); - - try { - if (value instanceof Map) { - return Array.from(value.entries()).map(([key, entryValue]) => [ - sanitizeLogValue(key, seen), - sanitizeLogValue(entryValue, seen), - ]); - } - - if (value instanceof Set) { - return Array.from(value.values()).map((entryValue) => sanitizeLogValue(entryValue, seen)); - } - - if (Array.isArray(value)) { - return value.map((entryValue) => sanitizeLogValue(entryValue, seen)); - } - - return Object.keys(value).reduce((safeValue, key) => { - try { - safeValue[key] = sanitizeLogValue(value[key], seen); - } catch (error) { - safeValue[key] = `[Unserializable: ${error.message}]`; - } - return safeValue; - }, {}); - } catch (error) { - return `[Unserializable: ${error.message}]`; - } finally { - seen.delete(value); - } + return sanitizeErrorValue(value, seen, { + includeStack: shouldExposeLogErrorStack(), + }); } /** @@ -210,6 +129,29 @@ function stringifyLogDetails(value) { } } +/** + * Normalize a failed build attempt into legacy message plus canonical details. + * + * @param {*} error - Raw failure error/value. + * @param {object} context - Build/subtask context for metadata. + * @returns {{ message: string, details: object }} Normalized failure payload. + */ +function normalizeFailureError(error, context = {}) { + const normalized = normalizeError(error || 'Unknown error', { + code: 'AIOX_EXECUTION_FAILED', + metadata: { + buildState: context, + }, + }); + + return { + message: normalized.message, + details: serializeError(normalized, { + includeStack: shouldExposeLogErrorStack(), + }), + }; +} + // ═══════════════════════════════════════════════════════════════════════════════════ // SCHEMA VALIDATION // ═══════════════════════════════════════════════════════════════════════════════════ @@ -930,12 +872,21 @@ class BuildStateManager { throw new Error('No state loaded'); } + const attempt = + options.attempt || + this._state.failedAttempts.filter((f) => f.subtaskId === subtaskId).length + 1; + + const normalizedFailure = normalizeFailureError(options.error, { + storyId: this.storyId, + subtaskId, + attempt, + }); + const failure = { subtaskId, - attempt: - options.attempt || - this._state.failedAttempts.filter((f) => f.subtaskId === subtaskId).length + 1, - error: options.error || 'Unknown error', + attempt, + error: normalizedFailure.message, + errorDetails: normalizedFailure.details, timestamp: new Date().toISOString(), approach: options.approach || null, duration: options.duration || null, @@ -957,6 +908,7 @@ class BuildStateManager { this._logAttempt(subtaskId, 'failure', { attempt: failure.attempt, error: failure.error, + errorDetails: failure.errorDetails, isStuck: isStuck.stuck, }); diff --git a/.aiox-core/core/ids/registry-updater.js b/.aiox-core/core/ids/registry-updater.js index ff1132358f..ba4d8b4d86 100644 --- a/.aiox-core/core/ids/registry-updater.js +++ b/.aiox-core/core/ids/registry-updater.js @@ -14,8 +14,11 @@ const { computeChecksum, resolveEntityId, resolveUsedBy, + buildNameIndex, + detectLifecycle, SCAN_CONFIG, ADAPTABILITY_DEFAULTS, + EXTERNAL_TOOLS, REPO_ROOT, REGISTRY_PATH, } = require(path.resolve(__dirname, '../../development/scripts/populate-entity-registry.js')); @@ -250,6 +253,7 @@ class RegistryUpdater { try { await this._withLock(async () => { const registry = this._loadRegistry(); + const changedEntities = []; for (const { action, filePath } of batch) { try { @@ -271,7 +275,20 @@ class RegistryUpdater { default: console.warn(`[IDS-Updater] Unknown action: ${action}`); } - if (mutated) updated++; + if (mutated) { + updated++; + if (action !== 'unlink') { + const relPath = path.relative(this._repoRoot, abs).replace(/\\/g, '/'); + const config = this._detectCategory(relPath); + if (config) { + const category = config.category; + const entityId = resolveEntityId(abs, config, registry.entities[category] || {}, this._repoRoot); + if (registry.entities[category]?.[entityId]) { + changedEntities.push({ category, entityId }); + } + } + } + } this._logAudit({ action, path: path.relative(this._repoRoot, abs).replace(/\\/g, '/'), trigger: 'watcher' }); } catch (err) { @@ -283,10 +300,13 @@ class RegistryUpdater { if (updated > 0) { this._resolveAllUsedBy(registry); + this._refreshDerivedDependencyFields(registry, changedEntities); // NOG-8: Apply code intelligence enrichment AFTER resolveAllUsedBy // so that code-intel usedBy data is merged on top of static graph await this._applyCodeIntelEnrichments(registry); + this._resolveAllUsedBy(registry); + this._refreshDerivedDependencyFields(registry, changedEntities); registry.metadata.lastUpdated = new Date().toISOString(); registry.metadata.entityCount = this._countEntities(registry); this._writeRegistry(registry); @@ -330,7 +350,7 @@ class RegistryUpdater { const resolvedEntityId = resolveEntityId(absPath, config, registry.entities[category], this._repoRoot); const keywords = extractKeywords(absPath, content); const purpose = extractPurpose(content, absPath); - const dependencies = detectDependencies(content, resolvedEntityId); + const dependencies = detectDependencies(content, resolvedEntityId, false, absPath); const checksum = computeChecksum(absPath); const defaultScore = ADAPTABILITY_DEFAULTS[config.type] || 0.5; @@ -342,6 +362,9 @@ class RegistryUpdater { keywords, usedBy: [], dependencies, + externalDeps: [], + plannedDeps: [], + lifecycle: 'experimental', adaptability: { score: defaultScore, constraints: [], @@ -383,12 +406,15 @@ class RegistryUpdater { } const newChecksum = computeChecksum(absPath); + const nextDependencies = detectDependencies(content, entityId, false, absPath); if (newChecksum !== existing.checksum) { existing.checksum = newChecksum; existing.purpose = extractPurpose(content, absPath); existing.keywords = extractKeywords(absPath, content); - existing.dependencies = detectDependencies(content, entityId); + existing.dependencies = nextDependencies; + } else if (JSON.stringify(existing.dependencies || []) !== JSON.stringify(nextDependencies)) { + existing.dependencies = nextDependencies; } existing.lastVerified = new Date().toISOString(); @@ -612,6 +638,45 @@ class RegistryUpdater { resolveUsedBy(registry.entities); } + _refreshDerivedDependencyFields(registry, changedEntities) { + const nameIndex = buildNameIndex(registry.entities); + const seen = new Set(); + + for (const { category, entityId } of changedEntities) { + const key = `${category}:${entityId}`; + if (seen.has(key)) continue; + seen.add(key); + + const entity = registry.entities[category]?.[entityId]; + if (!entity) continue; + + const internal = []; + const external = []; + const planned = []; + + const rawDeps = [ + ...(entity.dependencies || []), + ...(entity.externalDeps || []), + ...(entity.plannedDeps || []), + ]; + + for (const dep of rawDeps) { + if (nameIndex.has(dep)) { + internal.push(dep); + } else if (EXTERNAL_TOOLS.has(String(dep).toLowerCase())) { + external.push(dep); + } else { + planned.push(dep); + } + } + + entity.dependencies = [...new Set(internal)]; + entity.externalDeps = [...new Set(external)]; + entity.plannedDeps = [...new Set(planned)]; + entity.lifecycle = detectLifecycle(entityId, entity); + } + } + _countEntities(registry) { let count = 0; for (const catEntities of Object.values(registry.entities)) { diff --git a/.aiox-core/core/synapse/engine.js b/.aiox-core/core/synapse/engine.js index 6b40de180d..022159f16a 100644 --- a/.aiox-core/core/synapse/engine.js +++ b/.aiox-core/core/synapse/engine.js @@ -12,6 +12,7 @@ const fs = require('fs'); const path = require('path'); +const { normalizeError, serializeError } = require('../errors'); const { estimateContextPercent, @@ -131,10 +132,20 @@ class PipelineMetrics { existing.end = endTime; existing.duration = Number(endTime - existing.start) / 1e6; } + const normalizedError = normalizeError(error, { + code: 'AIOX_SYNAPSE_LAYER_FAILED', + metadata: { + synapse: { + layer: name, + }, + }, + }); + this.layers[name] = { ...existing, status: 'error', - error: error && error.message ? error.message : String(error), + error: normalizedError.message, + errorDetails: serializeError(normalizedError), }; } diff --git a/.aiox-core/data/entity-registry.yaml b/.aiox-core/data/entity-registry.yaml index 058a6ba2bf..f2ebb9ed83 100644 --- a/.aiox-core/data/entity-registry.yaml +++ b/.aiox-core/data/entity-registry.yaml @@ -1,7 +1,7 @@ metadata: version: 1.0.0 - lastUpdated: '2026-05-08T10:11:41.100Z' - entityCount: 752 + lastUpdated: '2026-05-08T14:34:39.794Z' + entityCount: 753 checksumAlgorithm: sha256 resolutionRate: 100 entities: @@ -7453,8 +7453,8 @@ entities: score: 0.7 constraints: [] extensionPoints: [] - checksum: sha256:31c29851e506246818f17564e7e67d82c1cc52e6108b40cd740e0415a96cdc77 - lastVerified: '2026-05-08T10:11:41.097Z' + checksum: sha256:418fcbf6ca3a6259f8d13e3290e8e49d6a16917b304039a6470264c5fdece89e + lastVerified: '2026-05-08T14:34:39.788Z' refactoring-suggester: path: .aiox-core/development/scripts/refactoring-suggester.js layer: L2 @@ -8711,6 +8711,7 @@ entities: - build-orchestrator - dev dependencies: + - errors-index - stuck-detector - recovery-tracker externalDeps: [] @@ -8720,8 +8721,8 @@ entities: score: 0.4 constraints: [] extensionPoints: [] - checksum: sha256:595523caf6db26624e7a483489345b9498ae15d99c2568073a2c0c45d5b46a54 - lastVerified: '2026-03-11T00:48:55.878Z' + checksum: sha256:02bafb4a29e7f769ae775fbc4e571ad34882569665f61ebc28eb87f7043eafc8 + lastVerified: '2026-05-08T14:34:39.786Z' context-injector: path: .aiox-core/core/execution/context-injector.js layer: L1 @@ -9181,8 +9182,8 @@ entities: score: 0.4 constraints: [] extensionPoints: [] - checksum: sha256:ed794ec39dc294567814693033dccf43669b177500c215fb70d6c3fc5b691527 - lastVerified: '2026-05-08T10:11:41.097Z' + checksum: sha256:00ae2e78486a9e7b3c8bf8e9ca0e9e955211ee4ae795c630e7f2934840d5596d + lastVerified: '2026-05-08T14:34:39.787Z' verification-gate: path: .aiox-core/core/ids/verification-gate.js layer: L1 @@ -11455,7 +11456,8 @@ entities: keywords: - context - builder - usedBy: [] + usedBy: + - synapse-engine dependencies: [] externalDeps: [] plannedDeps: [] @@ -11476,6 +11478,7 @@ entities: - tracker usedBy: - pipeline-collector + - synapse-engine dependencies: [] externalDeps: [] plannedDeps: [] @@ -11763,7 +11766,8 @@ entities: keywords: - memory - bridge - usedBy: [] + usedBy: + - synapse-engine dependencies: - tokens - synapse-memory-provider @@ -11806,7 +11810,8 @@ entities: purpose: Entity at .aiox-core/core/synapse/output/formatter.js keywords: - formatter - usedBy: [] + usedBy: + - synapse-engine dependencies: - tokens externalDeps: [] @@ -12863,7 +12868,9 @@ entities: purpose: Entity at .aiox-core/core/errors/index.js keywords: - index - usedBy: [] + usedBy: + - build-state-manager + - synapse-engine dependencies: - constants - error-registry @@ -12914,6 +12921,29 @@ entities: extensionPoints: [] checksum: sha256:24b8ff80e98efc2b562843262490b7ac537ee77565715b644f212652192ced62 lastVerified: '2026-05-08T10:11:41.096Z' + synapse-engine: + path: .aiox-core/core/synapse/engine.js + layer: L1 + type: module + purpose: Entity at .aiox-core/core/synapse/engine.js + keywords: + - engine + usedBy: [] + dependencies: + - errors-index + - context-tracker + - context-builder + - formatter + - memory-bridge + adaptability: + score: 0.4 + constraints: [] + extensionPoints: [] + checksum: sha256:1b65523b2fec6a44ab380298c4d98f0569209dec8b50be3922479ffcd6548df0 + lastVerified: '2026-05-08T14:34:39.788Z' + externalDeps: [] + plannedDeps: [] + lifecycle: experimental agents: aiox-master: path: .aiox-core/development/agents/aiox-master.md diff --git a/.aiox-core/development/scripts/populate-entity-registry.js b/.aiox-core/development/scripts/populate-entity-registry.js index 6ddc7b1b06..dbebf4f928 100644 --- a/.aiox-core/development/scripts/populate-entity-registry.js +++ b/.aiox-core/development/scripts/populate-entity-registry.js @@ -49,6 +49,7 @@ const EXTERNAL_TOOLS = new Set([ ]); const DEPRECATED_PATTERNS = [/^old[-_]/, /^backup[-_]/, /deprecated/i, /^legacy[-_]/]; +const INDEX_RESOLUTION_EXTENSIONS = ['.js', '.mjs', '.ts', '.yaml', '.yml', '.md']; const SENTINEL_VALUES = new Set(['n/a', 'na', 'none', 'tbd', 'todo', '-', '']); @@ -111,6 +112,42 @@ function resolveEntityId(filePath, config, existingEntities = {}, repoRoot = REP return candidate; } +function findScanConfigForPath(filePath, repoRoot = REPO_ROOT) { + const relPath = path.relative(repoRoot, filePath).replace(/\\/g, '/'); + return SCAN_CONFIG.find((config) => { + const basePath = config.basePath.replace(/\\/g, '/').replace(/\/+$/, ''); + return relPath === basePath || relPath.startsWith(`${basePath}/`); + }) || null; +} + +function resolveRelativeDependencyId(refPath, currentFilePath) { + if (!currentFilePath || !(refPath.startsWith('.') || refPath.startsWith('/'))) { + return null; + } + + const resolvedPath = path.resolve(path.dirname(currentFilePath), refPath); + const resolvedExt = path.extname(resolvedPath); + + if (resolvedExt) { + if (path.basename(resolvedPath, resolvedExt) === 'index') { + const config = findScanConfigForPath(resolvedPath); + if (config) return toScopedEntityId(resolvedPath, config); + } + return path.basename(resolvedPath, resolvedExt); + } + + for (const ext of INDEX_RESOLUTION_EXTENSIONS) { + const indexPath = path.join(resolvedPath, `index${ext}`); + if (fs.existsSync(indexPath)) { + const config = findScanConfigForPath(indexPath); + if (config) return toScopedEntityId(indexPath, config); + return path.basename(resolvedPath); + } + } + + return path.basename(resolvedPath); +} + function extractKeywords(filePath, content) { const name = path.basename(filePath, path.extname(filePath)); const parts = name.split(/[-_.]/g).filter((p) => p.length > 1); @@ -302,15 +339,16 @@ function extractMarkdownCrossReferences(content, entityId, verbose = false) { return [...deps]; } -function detectDependencies(content, entityId, verbose = false) { +function detectDependencies(content, entityId, verbose = false, currentFilePath = null) { const deps = new Set(); const requireMatches = content.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g); for (const m of requireMatches) { const reqPath = m[1]; if (reqPath.startsWith('.') || reqPath.startsWith('/')) { - const base = path.basename(reqPath, path.extname(reqPath)); - if (base !== entityId) deps.add(base); + const dependencyId = resolveRelativeDependencyId(reqPath, currentFilePath) + || path.basename(reqPath, path.extname(reqPath)); + if (dependencyId !== entityId) deps.add(dependencyId); } } @@ -318,8 +356,9 @@ function detectDependencies(content, entityId, verbose = false) { for (const m of importMatches) { const impPath = m[1]; if (impPath.startsWith('.') || impPath.startsWith('/')) { - const base = path.basename(impPath, path.extname(impPath)); - if (base !== entityId) deps.add(base); + const dependencyId = resolveRelativeDependencyId(impPath, currentFilePath) + || path.basename(impPath, path.extname(impPath)); + if (dependencyId !== entityId) deps.add(dependencyId); } } @@ -371,7 +410,7 @@ function scanCategory(config, verbose = false) { const relPath = path.relative(REPO_ROOT, filePath).replace(/\\/g, '/'); const keywords = extractKeywords(filePath, content); const purpose = extractPurpose(content, filePath); - const baseDeps = detectDependencies(content, entityId, verbose); + const baseDeps = detectDependencies(content, entityId, verbose, filePath); // Semantic YAML extraction for agents and workflows const yamlCategories = ['agents', 'workflows']; @@ -677,6 +716,8 @@ module.exports = { extractEntityId, toScopedEntityId, resolveEntityId, + findScanConfigForPath, + resolveRelativeDependencyId, extractKeywords, extractPurpose, detectDependencies, diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 73ac8136be..fb49fe3e98 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.1.15 -generated_at: "2026-05-08T10:11:41.844Z" +generated_at: "2026-05-08T14:35:03.596Z" generator: scripts/generate-install-manifest.js file_count: 1117 files: @@ -433,9 +433,9 @@ files: type: core size: 33071 - path: core/execution/build-state-manager.js - hash: sha256:7a6127abb4cd23b83563e55065b6a4771764972d40d87ba08117ee8c0c48980e + hash: sha256:02bafb4a29e7f769ae775fbc4e571ad34882569665f61ebc28eb87f7043eafc8 type: core - size: 54015 + size: 53043 - path: core/execution/context-injector.js hash: sha256:7654f6e6c3d555f3963369e71edf84b15c0fdec53bd161800b71782f78275244 type: core @@ -777,9 +777,9 @@ files: type: core size: 8096 - path: core/ids/registry-updater.js - hash: sha256:ed794ec39dc294567814693033dccf43669b177500c215fb70d6c3fc5b691527 + hash: sha256:00ae2e78486a9e7b3c8bf8e9ca0e9e955211ee4ae795c630e7f2934840d5596d type: core - size: 24686 + size: 27005 - path: core/ids/verification-gate.js hash: sha256:96050661c90fa52bfc755911d02c9194ec35c00e71fc6bbc92a13686dd53bb91 type: core @@ -1153,9 +1153,9 @@ files: type: core size: 8122 - path: core/synapse/engine.js - hash: sha256:cd26f9a88527744fa4984fc006c428563d6a1ef023a9feb2dd8da073b4e70b9f + hash: sha256:1b65523b2fec6a44ab380298c4d98f0569209dec8b50be3922479ffcd6548df0 type: core - size: 14362 + size: 14631 - path: core/synapse/layers/l0-constitution.js hash: sha256:2123a6a44915aaac2a6bbd26c67c285c9d1e12b50fe42a8ada668306b07d1c4a type: core @@ -1265,9 +1265,9 @@ files: type: data size: 9590 - path: data/entity-registry.yaml - hash: sha256:749b0ef51090a7787772dc9b07e604e6ff3403e32d1d59cf04a2229d68801543 + hash: sha256:9f68c1c624f34243acbd1205cde7989d58aeefc71692f17ea3c92afa9e69877a type: data - size: 525678 + size: 526494 - path: data/learned-patterns.yaml hash: sha256:24ac0b160615583a0ff783d3da8af80b7f94191575d6db2054ec8e10a3f945dc type: data @@ -1633,9 +1633,9 @@ files: type: script size: 23410 - path: development/scripts/populate-entity-registry.js - hash: sha256:31c29851e506246818f17564e7e67d82c1cc52e6108b40cd740e0415a96cdc77 + hash: sha256:418fcbf6ca3a6259f8d13e3290e8e49d6a16917b304039a6470264c5fdece89e type: script - size: 23451 + size: 25077 - path: development/scripts/refactoring-suggester.js hash: sha256:d50ea6b609c9cf8385979386fee4b4385d11ebcde15460260f66d04c705f6cd9 type: script diff --git a/docs/stories/epic-error-governance/EPIC-AIOX-ERROR-GOVERNANCE.md b/docs/stories/epic-error-governance/EPIC-AIOX-ERROR-GOVERNANCE.md index 0fd789156d..519970a353 100644 --- a/docs/stories/epic-error-governance/EPIC-AIOX-ERROR-GOVERNANCE.md +++ b/docs/stories/epic-error-governance/EPIC-AIOX-ERROR-GOVERNANCE.md @@ -6,7 +6,7 @@ |-------|-------| | Epic ID | AIOX-ERROR-GOVERNANCE | | Status | In Progress | -| Source Issue | #701 | +| Source Issue | #701, #621 | | Parent | #621 | | Priority | P1 | @@ -35,4 +35,5 @@ silenciosas de hooks. | Story | Título | Status | |-------|--------|--------| -| AIOX-ERROR.1 | Core Error Contract | Ready for Review | +| AIOX-ERROR.1 | Core Error Contract | Done | +| AIOX-ERROR.2 | Serialization-Sensitive Migration | Done | diff --git a/docs/stories/epic-error-governance/STORY-AIOX-ERROR.2-SERIALIZATION-SENSITIVE-MIGRATION.md b/docs/stories/epic-error-governance/STORY-AIOX-ERROR.2-SERIALIZATION-SENSITIVE-MIGRATION.md new file mode 100644 index 0000000000..ba5c18c540 --- /dev/null +++ b/docs/stories/epic-error-governance/STORY-AIOX-ERROR.2-SERIALIZATION-SENSITIVE-MIGRATION.md @@ -0,0 +1,94 @@ +# Story AIOX-ERROR.2: Serialization-Sensitive Migration + +## Metadata + +| Campo | Valor | +|-------|-------| +| Story ID | AIOX-ERROR.2 | +| Epic | AIOX-ERROR-GOVERNANCE | +| Status | Done | +| Executor | @dev | +| Quality Gate | @qa | +| Points | 5 | +| Prioridade | P1 | +| Source Issue | #621 | + +## Objetivo + +Migrar os primeiros pontos sensíveis de serialização para o contrato canônico de +Error Governance, preservando compatibilidade com consumidores existentes. + +## Acceptance Criteria + +- [x] AC1: `BuildStateManager` usa a serialização canônica para logs e detalhes + de falha sem alterar o campo legado `error` como mensagem. +- [x] AC2: `BuildStateManager.recordFailure()` preserva metadata estruturada em + `errorDetails`, incluindo stack redigido por padrão, `cause`, propriedades + próprias e contexto de story/subtask. +- [x] AC3: `PipelineMetrics.errorLayer()` preserva o campo legado `error` como + mensagem e adiciona `errorDetails` canônico para falhas de camada Synapse. +- [x] AC4: A mudança não altera políticas silenciosas de hooks, saída CLI ou + exit codes existentes. +- [x] AC5: Testes focados de build-state, synapse e errors passam. + +## Tasks + +- [x] T1: Validar estado vivo de `main`, PRs e issues antes de implementar. +- [x] T2: Migrar logs/falhas do `BuildStateManager` para serializer canônico. +- [x] T3: Migrar métricas de erro do `SynapseEngine` para detalhes canônicos. +- [x] T4: Atualizar testes focados de compatibilidade. +- [x] T5: Rodar gates focados e atualizar registros gerados. + +## Dev Agent Record + +### Debug Log + +- Checkpoint vivo: `main` alinhada com `origin/main` em `7f22246b`, sem PRs + abertos, #701 fechado, #621 aberto. +- Branch: `feature/issue-621-error-normalizer-migration`. +- Testes focados: `npm test -- --runTestsByPath tests/core/build-state-manager.test.js tests/synapse/engine.test.js tests/core/errors/aiox-error.test.js tests/core/errors/serializer.test.js --runInBand` passou com 112 testes. +- Lint focado: `npx eslint .aiox-core/core/execution/build-state-manager.js .aiox-core/core/synapse/engine.js tests/core/build-state-manager.test.js tests/synapse/engine.test.js --no-warn-ignored` passou sem erros. +- Manifesto: `npm run generate:manifest` e `npm run validate:manifest` passaram. +- Typecheck: `npm run typecheck` passou. +- Lint completo: `npm run lint` passou com 0 erros e 114 warnings preexistentes de `comma-dangle`. +- Testes completos: `npm test -- --runInBand --forceExit --silent` passou com 334 suites, 8.374 testes passados e 149 skipped. +- CodeRabbit PR #703: corrigido registry incremental para resolver imports de diretórios + `index.js` como IDs escopados, evitando dependência órfã `errors`, e para reconciliar campos padrão em entidades já existentes. +- Testes focados pós-CodeRabbit: `npm test -- --runTestsByPath tests/core/build-state-manager.test.js tests/synapse/engine.test.js tests/core/errors/aiox-error.test.js tests/core/errors/serializer.test.js tests/core/ids/populate-entity-registry.test.js --runInBand` passou com 186 testes. +- Segunda revisão CodeRabbit PR #703: `findScanConfigForPath()` passou a respeitar boundaries de diretório e o updater passou a recalcular `externalDeps`, `plannedDeps` e `lifecycle` após refresh de dependências. Testes focados passaram com 187 testes. +- Ajuste de CI PR #703: recomputação do `RegistryUpdater` tornou-se idempotente preservando `dependencies`, `externalDeps` e `plannedDeps`; teste alinhado à classificação de dependências não resolvidas em `plannedDeps`. +- Revalidação local pós-ajuste CI: `npm run typecheck` passou; `npm test -- --runInBand --forceExit --silent` passou com 334 suites, 8.376 testes passados e 149 skipped. + +### Completion Notes + +- `BuildStateManager` preserva o campo legado `error` como mensagem e adiciona `errorDetails` canônico em falhas persistidas e logs. +- `PipelineMetrics.errorLayer()` preserva `error` como mensagem e adiciona `errorDetails` canônico para falhas de camada Synapse. +- Registros IDS e install manifest foram atualizados e validados, incluindo correção para dependências `errors-index`. + +### File List + +| File | Action | +|------|--------| +| `docs/stories/epic-error-governance/EPIC-AIOX-ERROR-GOVERNANCE.md` | Updated | +| `docs/stories/epic-error-governance/STORY-AIOX-ERROR.2-SERIALIZATION-SENSITIVE-MIGRATION.md` | Created | +| `.aiox-core/core/execution/build-state-manager.js` | Updated | +| `.aiox-core/core/synapse/engine.js` | Updated | +| `.aiox-core/core/ids/registry-updater.js` | Updated | +| `.aiox-core/development/scripts/populate-entity-registry.js` | Updated | +| `.aiox-core/data/entity-registry.yaml` | Updated | +| `.aiox-core/install-manifest.yaml` | Updated | +| `tests/core/build-state-manager.test.js` | Updated | +| `tests/synapse/engine.test.js` | Updated | +| `tests/core/ids/populate-entity-registry.test.js` | Updated | + +### Change Log + +| Data | Agente | Mudança | +|------|--------|---------| +| 2026-05-08 | Codex | Story criada e primeira migração sensível de serialização iniciada. | +| 2026-05-08 | Codex | `BuildStateManager` e `PipelineMetrics` migrados para detalhes de erro canônicos com testes focados verdes. | +| 2026-05-08 | Codex | Gates completos executados e story marcada como Done. | +| 2026-05-08 | Codex | Comentários do CodeRabbit no PR #703 corrigidos no registry updater/extractor. | +| 2026-05-08 | Codex | Segunda rodada CodeRabbit corrigida com recomputação de campos derivados do registry. | +| 2026-05-08 | Codex | Falha Node 22 do CI corrigida preservando `plannedDeps` em refresh repetido e ajustando teste de dependência ainda não registrada. | +| 2026-05-08 | Codex | Revalidação local completa executada após ajuste de CI. | diff --git a/tests/core/build-state-manager.test.js b/tests/core/build-state-manager.test.js index 3c6b2f9064..4bd8d034b0 100644 --- a/tests/core/build-state-manager.test.js +++ b/tests/core/build-state-manager.test.js @@ -501,12 +501,61 @@ describe('BuildStateManager', () => { expect(result.failure.subtaskId).toBe('1.1'); expect(result.failure.error).toBe('Test error'); + expect(result.failure.errorDetails).toEqual(expect.objectContaining({ + name: 'AIOXError', + code: 'AIOX_EXECUTION_FAILED', + stack: '[redacted]', + })); const state = manager.getState(); expect(state.failedAttempts).toHaveLength(1); expect(state.metrics.totalFailures).toBe(1); }); + test('should preserve structured failure metadata for Error inputs', () => { + const cause = new Error('root cause'); + const error = new TypeError('Typed failure'); + error.cause = cause; + error.exitCode = 2; + + const result = manager.recordFailure('1.structured', { + error, + attempt: 1, + }); + + expect(result.failure.error).toBe('Typed failure'); + expect(result.failure.errorDetails).toEqual(expect.objectContaining({ + name: 'AIOXError', + message: 'Typed failure', + code: 'AIOX_EXECUTION_FAILED', + category: 'execution', + stack: '[redacted]', + metadata: expect.objectContaining({ + buildState: expect.objectContaining({ + storyId: testStoryId, + subtaskId: '1.structured', + attempt: 1, + }), + originalError: expect.objectContaining({ + name: 'TypeError', + properties: expect.objectContaining({ + exitCode: 2, + }), + }), + }), + cause: expect.objectContaining({ + name: 'TypeError', + message: 'Typed failure', + stack: '[redacted]', + cause: expect.objectContaining({ + name: 'Error', + message: 'root cause', + stack: '[redacted]', + }), + }), + })); + }); + test('should track multiple failures', () => { manager.recordFailure('1.1', { error: 'Error 1' }); manager.recordFailure('1.1', { error: 'Error 2' }); @@ -623,6 +672,7 @@ describe('BuildStateManager', () => { expect(content).toContain('"name":"TypeError"'); expect(content).toContain('"message":"boom"'); expect(content).toContain('"stack":"[redacted]"'); + expect(content).toContain('"cause":{"name":"Error","message":"nested cause","stack":"[redacted]"}'); expect(content).toContain('[["attempt","3"]]'); expect(content).toContain('"set":["2"]'); expect(content).toMatch(/"callback":"[^"]*ignored[^"]*"/); diff --git a/tests/core/ids/populate-entity-registry.test.js b/tests/core/ids/populate-entity-registry.test.js index 1ace748669..799b1fa64f 100644 --- a/tests/core/ids/populate-entity-registry.test.js +++ b/tests/core/ids/populate-entity-registry.test.js @@ -11,6 +11,7 @@ const { extractMarkdownCrossReferences, resolveEntityId, toScopedEntityId, + findScanConfigForPath, computeChecksum, scanCategory, resolveUsedBy, @@ -106,6 +107,15 @@ describe('populate-entity-registry (AC: 3, 4, 12)', () => { }); }); + describe('findScanConfigForPath()', () => { + it('matches scan config only on directory boundaries', () => { + const repoRoot = '/repo'; + + expect(findScanConfigForPath('/repo/.aiox-core/core/errors/index.js', repoRoot)?.category).toBe('modules'); + expect(findScanConfigForPath('/repo/.aiox-core/corex/errors/index.js', repoRoot)).toBeNull(); + }); + }); + describe('extractKeywords()', () => { it('extracts keywords from filename', () => { const kws = extractKeywords('create-doc-template.md', ''); @@ -177,6 +187,16 @@ describe('populate-entity-registry (AC: 3, 4, 12)', () => { expect(deps).toContain('bar'); }); + it('resolves relative directory index dependencies to scoped entity ids', () => { + const content = "const { normalizeError } = require('../errors');"; + const currentFilePath = path.resolve(__dirname, '../../../.aiox-core/core/synapse/engine.js'); + + const deps = detectDependencies(content, 'synapse-engine', false, currentFilePath); + + expect(deps).toContain('errors-index'); + expect(deps).not.toContain('errors'); + }); + it('detects import dependencies', () => { const content = "import { something } from './my-util';\nimport other from '../other-lib';"; const deps = detectDependencies(content, 'main'); diff --git a/tests/core/ids/registry-updater.test.js b/tests/core/ids/registry-updater.test.js index eb9c5bf5bf..7ed96d3f1c 100644 --- a/tests/core/ids/registry-updater.test.js +++ b/tests/core/ids/registry-updater.test.js @@ -188,7 +188,7 @@ describe('RegistryUpdater', () => { expect(entity.keywords).toEqual(expect.arrayContaining(['deploy', 'automation'])); }); - it('detects dependencies from require statements', async () => { + it('classifies unresolved require dependencies as planned dependencies', async () => { const updater = createUpdater(); const filePath = createTempFile( '.aiox-core/development/scripts/consumer.js', @@ -200,7 +200,8 @@ describe('RegistryUpdater', () => { const registry = readRegistry(); const entity = registry.entities.scripts.consumer; expect(entity).toBeDefined(); - expect(entity.dependencies).toContain('helper'); + expect(entity.dependencies).not.toContain('helper'); + expect(entity.plannedDeps).toContain('helper'); }); it('creates category if it does not exist', async () => { diff --git a/tests/synapse/engine.test.js b/tests/synapse/engine.test.js index cf1f7b76d8..e1b606370f 100644 --- a/tests/synapse/engine.test.js +++ b/tests/synapse/engine.test.js @@ -163,6 +163,12 @@ describe('PipelineMetrics', () => { metrics.errorLayer('keyword', new Error('File not found')); expect(metrics.layers.keyword.status).toBe('error'); expect(metrics.layers.keyword.error).toBe('File not found'); + expect(metrics.layers.keyword.errorDetails).toEqual(expect.objectContaining({ + name: 'AIOXError', + code: 'AIOX_SYNAPSE_LAYER_FAILED', + category: 'synapse', + stack: '[redacted]', + })); }); test('errorLayer() should record duration if startLayer() was called', () => { @@ -176,6 +182,41 @@ describe('PipelineMetrics', () => { test('errorLayer() should handle non-Error objects', () => { metrics.errorLayer('test', 'string error'); expect(metrics.layers.test.error).toBe('string error'); + expect(metrics.layers.test.errorDetails).toEqual(expect.objectContaining({ + name: 'AIOXError', + code: 'AIOX_SYNAPSE_LAYER_FAILED', + message: 'string error', + })); + }); + + test('errorLayer() should preserve canonical metadata without changing legacy message', () => { + const error = new TypeError('Layer typed failure'); + error.layerPhase = 'parse'; + + metrics.errorLayer('workflow', error); + + expect(metrics.layers.workflow.error).toBe('Layer typed failure'); + expect(metrics.layers.workflow.errorDetails).toEqual(expect.objectContaining({ + name: 'AIOXError', + message: 'Layer typed failure', + code: 'AIOX_SYNAPSE_LAYER_FAILED', + category: 'synapse', + metadata: expect.objectContaining({ + synapse: { layer: 'workflow' }, + originalError: expect.objectContaining({ + name: 'TypeError', + properties: expect.objectContaining({ + layerPhase: 'parse', + }), + }), + }), + cause: expect.objectContaining({ + name: 'TypeError', + message: 'Layer typed failure', + stack: '[redacted]', + layerPhase: 'parse', + }), + })); }); test('getSummary() should return correct totals', () => {