Skip to content

Commit 8bbb7d2

Browse files
committed
fix(core): migrate error serialization surfaces [Story AIOX-ERROR.2]
1 parent 7f22246 commit 8bbb7d2

11 files changed

Lines changed: 423 additions & 128 deletions

File tree

.aiox-core/core/execution/build-state-manager.js

Lines changed: 45 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
const fs = require('fs');
2424
const path = require('path');
2525
const crypto = require('crypto');
26+
const {
27+
normalizeError,
28+
sanitizeValue: sanitizeErrorValue,
29+
serializeError,
30+
} = require('../errors');
2631

2732
// Optional dependencies with graceful fallback
2833
let chalk;
@@ -95,95 +100,9 @@ const NotificationType = {
95100
* @returns {*} Data-only representation that JSON.stringify can serialize.
96101
*/
97102
function sanitizeLogValue(value, seen = new WeakSet()) {
98-
if (value === undefined || value === null) {
99-
return value;
100-
}
101-
102-
const valueType = typeof value;
103-
104-
if (valueType === 'bigint') {
105-
return value.toString();
106-
}
107-
108-
if (valueType === 'function' || valueType === 'symbol') {
109-
return String(value);
110-
}
111-
112-
if (valueType !== 'object') {
113-
return value;
114-
}
115-
116-
if (seen.has(value)) {
117-
return '[Circular]';
118-
}
119-
120-
if (value instanceof Date) {
121-
return Number.isNaN(value.getTime()) ? value.toString() : value.toISOString();
122-
}
123-
124-
if (value instanceof RegExp) {
125-
return value.toString();
126-
}
127-
128-
if (value instanceof Error) {
129-
const safeError = {
130-
name: value.name,
131-
message: value.message,
132-
stack: shouldExposeLogErrorStack() ? value.stack : '[redacted]',
133-
};
134-
135-
seen.add(value);
136-
137-
try {
138-
Object.getOwnPropertyNames(value).forEach((key) => {
139-
if (key === 'name' || key === 'message' || key === 'stack') {
140-
return;
141-
}
142-
143-
try {
144-
safeError[key] = sanitizeLogValue(value[key], seen);
145-
} catch (error) {
146-
safeError[key] = `[Unserializable: ${error.message}]`;
147-
}
148-
});
149-
150-
return safeError;
151-
} finally {
152-
seen.delete(value);
153-
}
154-
}
155-
156-
seen.add(value);
157-
158-
try {
159-
if (value instanceof Map) {
160-
return Array.from(value.entries()).map(([key, entryValue]) => [
161-
sanitizeLogValue(key, seen),
162-
sanitizeLogValue(entryValue, seen),
163-
]);
164-
}
165-
166-
if (value instanceof Set) {
167-
return Array.from(value.values()).map((entryValue) => sanitizeLogValue(entryValue, seen));
168-
}
169-
170-
if (Array.isArray(value)) {
171-
return value.map((entryValue) => sanitizeLogValue(entryValue, seen));
172-
}
173-
174-
return Object.keys(value).reduce((safeValue, key) => {
175-
try {
176-
safeValue[key] = sanitizeLogValue(value[key], seen);
177-
} catch (error) {
178-
safeValue[key] = `[Unserializable: ${error.message}]`;
179-
}
180-
return safeValue;
181-
}, {});
182-
} catch (error) {
183-
return `[Unserializable: ${error.message}]`;
184-
} finally {
185-
seen.delete(value);
186-
}
103+
return sanitizeErrorValue(value, seen, {
104+
includeStack: shouldExposeLogErrorStack(),
105+
});
187106
}
188107

189108
/**
@@ -210,6 +129,29 @@ function stringifyLogDetails(value) {
210129
}
211130
}
212131

132+
/**
133+
* Normalize a failed build attempt into legacy message plus canonical details.
134+
*
135+
* @param {*} error - Raw failure error/value.
136+
* @param {object} context - Build/subtask context for metadata.
137+
* @returns {{ message: string, details: object }} Normalized failure payload.
138+
*/
139+
function normalizeFailureError(error, context = {}) {
140+
const normalized = normalizeError(error || 'Unknown error', {
141+
code: 'AIOX_EXECUTION_FAILED',
142+
metadata: {
143+
buildState: context,
144+
},
145+
});
146+
147+
return {
148+
message: normalized.message,
149+
details: serializeError(normalized, {
150+
includeStack: shouldExposeLogErrorStack(),
151+
}),
152+
};
153+
}
154+
213155
// ═══════════════════════════════════════════════════════════════════════════════════
214156
// SCHEMA VALIDATION
215157
// ═══════════════════════════════════════════════════════════════════════════════════
@@ -930,12 +872,21 @@ class BuildStateManager {
930872
throw new Error('No state loaded');
931873
}
932874

875+
const attempt =
876+
options.attempt ||
877+
this._state.failedAttempts.filter((f) => f.subtaskId === subtaskId).length + 1;
878+
879+
const normalizedFailure = normalizeFailureError(options.error, {
880+
storyId: this.storyId,
881+
subtaskId,
882+
attempt,
883+
});
884+
933885
const failure = {
934886
subtaskId,
935-
attempt:
936-
options.attempt ||
937-
this._state.failedAttempts.filter((f) => f.subtaskId === subtaskId).length + 1,
938-
error: options.error || 'Unknown error',
887+
attempt,
888+
error: normalizedFailure.message,
889+
errorDetails: normalizedFailure.details,
939890
timestamp: new Date().toISOString(),
940891
approach: options.approach || null,
941892
duration: options.duration || null,
@@ -957,6 +908,7 @@ class BuildStateManager {
957908
this._logAttempt(subtaskId, 'failure', {
958909
attempt: failure.attempt,
959910
error: failure.error,
911+
errorDetails: failure.errorDetails,
960912
isStuck: isStuck.stuck,
961913
});
962914

.aiox-core/core/ids/registry-updater.js

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ const {
1414
computeChecksum,
1515
resolveEntityId,
1616
resolveUsedBy,
17+
buildNameIndex,
18+
detectLifecycle,
1719
SCAN_CONFIG,
1820
ADAPTABILITY_DEFAULTS,
21+
EXTERNAL_TOOLS,
1922
REPO_ROOT,
2023
REGISTRY_PATH,
2124
} = require(path.resolve(__dirname, '../../development/scripts/populate-entity-registry.js'));
@@ -250,6 +253,7 @@ class RegistryUpdater {
250253
try {
251254
await this._withLock(async () => {
252255
const registry = this._loadRegistry();
256+
const changedEntities = [];
253257

254258
for (const { action, filePath } of batch) {
255259
try {
@@ -271,7 +275,20 @@ class RegistryUpdater {
271275
default:
272276
console.warn(`[IDS-Updater] Unknown action: ${action}`);
273277
}
274-
if (mutated) updated++;
278+
if (mutated) {
279+
updated++;
280+
if (action !== 'unlink') {
281+
const relPath = path.relative(this._repoRoot, abs).replace(/\\/g, '/');
282+
const config = this._detectCategory(relPath);
283+
if (config) {
284+
const category = config.category;
285+
const entityId = resolveEntityId(abs, config, registry.entities[category] || {}, this._repoRoot);
286+
if (registry.entities[category]?.[entityId]) {
287+
changedEntities.push({ category, entityId });
288+
}
289+
}
290+
}
291+
}
275292

276293
this._logAudit({ action, path: path.relative(this._repoRoot, abs).replace(/\\/g, '/'), trigger: 'watcher' });
277294
} catch (err) {
@@ -283,10 +300,13 @@ class RegistryUpdater {
283300

284301
if (updated > 0) {
285302
this._resolveAllUsedBy(registry);
303+
this._refreshDerivedDependencyFields(registry, changedEntities);
286304

287305
// NOG-8: Apply code intelligence enrichment AFTER resolveAllUsedBy
288306
// so that code-intel usedBy data is merged on top of static graph
289307
await this._applyCodeIntelEnrichments(registry);
308+
this._resolveAllUsedBy(registry);
309+
this._refreshDerivedDependencyFields(registry, changedEntities);
290310
registry.metadata.lastUpdated = new Date().toISOString();
291311
registry.metadata.entityCount = this._countEntities(registry);
292312
this._writeRegistry(registry);
@@ -330,7 +350,7 @@ class RegistryUpdater {
330350
const resolvedEntityId = resolveEntityId(absPath, config, registry.entities[category], this._repoRoot);
331351
const keywords = extractKeywords(absPath, content);
332352
const purpose = extractPurpose(content, absPath);
333-
const dependencies = detectDependencies(content, resolvedEntityId);
353+
const dependencies = detectDependencies(content, resolvedEntityId, false, absPath);
334354
const checksum = computeChecksum(absPath);
335355
const defaultScore = ADAPTABILITY_DEFAULTS[config.type] || 0.5;
336356

@@ -342,6 +362,9 @@ class RegistryUpdater {
342362
keywords,
343363
usedBy: [],
344364
dependencies,
365+
externalDeps: [],
366+
plannedDeps: [],
367+
lifecycle: 'experimental',
345368
adaptability: {
346369
score: defaultScore,
347370
constraints: [],
@@ -383,12 +406,15 @@ class RegistryUpdater {
383406
}
384407

385408
const newChecksum = computeChecksum(absPath);
409+
const nextDependencies = detectDependencies(content, entityId, false, absPath);
386410

387411
if (newChecksum !== existing.checksum) {
388412
existing.checksum = newChecksum;
389413
existing.purpose = extractPurpose(content, absPath);
390414
existing.keywords = extractKeywords(absPath, content);
391-
existing.dependencies = detectDependencies(content, entityId);
415+
existing.dependencies = nextDependencies;
416+
} else if (JSON.stringify(existing.dependencies || []) !== JSON.stringify(nextDependencies)) {
417+
existing.dependencies = nextDependencies;
392418
}
393419

394420
existing.lastVerified = new Date().toISOString();
@@ -612,6 +638,39 @@ class RegistryUpdater {
612638
resolveUsedBy(registry.entities);
613639
}
614640

641+
_refreshDerivedDependencyFields(registry, changedEntities) {
642+
const nameIndex = buildNameIndex(registry.entities);
643+
const seen = new Set();
644+
645+
for (const { category, entityId } of changedEntities) {
646+
const key = `${category}:${entityId}`;
647+
if (seen.has(key)) continue;
648+
seen.add(key);
649+
650+
const entity = registry.entities[category]?.[entityId];
651+
if (!entity) continue;
652+
653+
const internal = [];
654+
const external = [];
655+
const planned = [];
656+
657+
for (const dep of entity.dependencies || []) {
658+
if (nameIndex.has(dep)) {
659+
internal.push(dep);
660+
} else if (EXTERNAL_TOOLS.has(String(dep).toLowerCase())) {
661+
external.push(dep);
662+
} else {
663+
planned.push(dep);
664+
}
665+
}
666+
667+
entity.dependencies = [...new Set(internal)];
668+
entity.externalDeps = [...new Set(external)];
669+
entity.plannedDeps = [...new Set(planned)];
670+
entity.lifecycle = detectLifecycle(entityId, entity);
671+
}
672+
}
673+
615674
_countEntities(registry) {
616675
let count = 0;
617676
for (const catEntities of Object.values(registry.entities)) {

.aiox-core/core/synapse/engine.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
const fs = require('fs');
1414
const path = require('path');
15+
const { normalizeError, serializeError } = require('../errors');
1516

1617
const {
1718
estimateContextPercent,
@@ -131,10 +132,20 @@ class PipelineMetrics {
131132
existing.end = endTime;
132133
existing.duration = Number(endTime - existing.start) / 1e6;
133134
}
135+
const normalizedError = normalizeError(error, {
136+
code: 'AIOX_SYNAPSE_LAYER_FAILED',
137+
metadata: {
138+
synapse: {
139+
layer: name,
140+
},
141+
},
142+
});
143+
134144
this.layers[name] = {
135145
...existing,
136146
status: 'error',
137-
error: error && error.message ? error.message : String(error),
147+
error: normalizedError.message,
148+
errorDetails: serializeError(normalizedError),
138149
};
139150
}
140151

0 commit comments

Comments
 (0)