aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
307 lines (273 loc) • 8.82 kB
JavaScript
'use strict';
/**
* VerificationGate — IDS Story IDS-5a
*
* Abstract base class for IDS verification gates (G1-G6).
* Uses the Template Method pattern: subclasses implement _doVerify()
* while the base class handles timeout, circuit breaker, logging,
* and graceful degradation.
*
* Gate behavior:
* - Advisory gates (G1-G4): log and return suggestions, never block
* - Blocking gates (G5-G6): handled in IDS-5b
*
* Key principle: Development must NEVER be blocked by IDS failures.
*
* Source: ids-principles.md, story IDS-5a
*/
const { CircuitBreaker } = require('./circuit-breaker');
const DEFAULT_TIMEOUT_MS = 2000;
/**
* Build a GateResult structure.
* @param {object} fields
* @returns {object}
*/
function createGateResult(fields = {}) {
return {
gateId: fields.gateId || null,
agent: fields.agent || null,
timestamp: fields.timestamp || new Date().toISOString(),
context: fields.context || {},
result: {
passed: fields.passed !== undefined ? fields.passed : true,
blocking: fields.blocking !== undefined ? fields.blocking : false,
warnings: fields.warnings || [],
opportunities: fields.opportunities || [],
},
override: fields.override || null,
executionMs: fields.executionMs || 0,
circuitBreakerState: fields.circuitBreakerState || 'CLOSED',
};
}
class VerificationGate {
/**
* @param {object} config
* @param {string} config.gateId — Gate identifier (e.g., 'G1', 'G2')
* @param {string} config.agent — Agent identifier (e.g., '@pm', '@dev')
* @param {boolean} [config.blocking=false] — Whether this gate can block workflow
* @param {number} [config.timeoutMs=2000] — Timeout for gate execution
* @param {object} [config.circuitBreakerOptions] — Options for CircuitBreaker
* @param {Function} [config.logger] — Custom logger function (defaults to console)
*/
constructor(config = {}) {
if (!config.gateId) {
throw new Error('[IDS-Gate] gateId is required');
}
if (!config.agent) {
throw new Error('[IDS-Gate] agent is required');
}
this._gateId = config.gateId;
this._agent = config.agent;
this._blocking = config.blocking || false;
this._timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
this._circuitBreaker = new CircuitBreaker(config.circuitBreakerOptions || {});
this._logger = config.logger || console;
this._invocationCount = 0;
this._lastResult = null;
}
// ================================================================
// Public API
// ================================================================
/**
* Execute the verification gate with timeout and circuit breaker protection.
* Template Method: calls _doVerify() implemented by subclasses.
*
* @param {object} context — Gate-specific verification context
* @returns {Promise<object>} GateResult structure
*/
async verify(context = {}) {
this._invocationCount++;
const startTime = Date.now();
// Circuit breaker check: if open, warn-and-proceed
if (!this._circuitBreaker.isAllowed()) {
const stats = this._circuitBreaker.getStats();
this._log('warn', `Circuit breaker OPEN (${stats.totalTrips} trips). Skipping gate.`);
const result = createGateResult({
gateId: this._gateId,
agent: this._agent,
passed: true,
blocking: false,
warnings: [`Gate ${this._gateId} skipped: circuit breaker open`],
opportunities: [],
context,
executionMs: Date.now() - startTime,
circuitBreakerState: stats.state,
});
this._logInvocation(result);
this._lastResult = result;
return result;
}
try {
// Execute with timeout wrapper
const verifyResult = await this._executeWithTimeout(context);
const executionMs = Date.now() - startTime;
this._circuitBreaker.recordSuccess();
const result = createGateResult({
gateId: this._gateId,
agent: this._agent,
passed: verifyResult.passed !== undefined ? verifyResult.passed : true,
blocking: this._blocking && !verifyResult.passed,
warnings: verifyResult.warnings || [],
opportunities: verifyResult.opportunities || [],
override: verifyResult.override || null,
context,
executionMs,
circuitBreakerState: this._circuitBreaker.getState(),
});
this._logInvocation(result);
this._lastResult = result;
return result;
} catch (error) {
const executionMs = Date.now() - startTime;
this._circuitBreaker.recordFailure();
// Graceful degradation: log-and-proceed on error
this._log('warn', `Gate failed (${error.message}). Proceeding with warning.`);
const result = createGateResult({
gateId: this._gateId,
agent: this._agent,
passed: true,
blocking: false,
warnings: [`Gate ${this._gateId} error: ${error.message}`],
opportunities: [],
context,
executionMs,
circuitBreakerState: this._circuitBreaker.getState(),
});
this._logInvocation(result);
this._lastResult = result;
return result;
}
}
/**
* Get the gate identifier.
* @returns {string}
*/
getGateId() {
return this._gateId;
}
/**
* Get the agent this gate is associated with.
* @returns {string}
*/
getAgent() {
return this._agent;
}
/**
* Get whether this gate blocks workflow.
* @returns {boolean}
*/
isBlocking() {
return this._blocking;
}
/**
* Get invocation count.
* @returns {number}
*/
getInvocationCount() {
return this._invocationCount;
}
/**
* Get the last result.
* @returns {object|null}
*/
getLastResult() {
return this._lastResult;
}
/**
* Get circuit breaker stats.
* @returns {object}
*/
getCircuitBreakerStats() {
return this._circuitBreaker.getStats();
}
// ================================================================
// Template Method — subclasses must implement
// ================================================================
/**
* Perform the actual verification logic.
* Subclasses MUST override this method.
*
* @param {object} context — Gate-specific context
* @returns {Promise<object>} Object with { passed, warnings, opportunities }
* @abstract
*/
async _doVerify(_context) {
throw new Error(
`[IDS-Gate] _doVerify() must be implemented by subclass (gate: ${this._gateId})`,
);
}
// ================================================================
// Internal helpers
// ================================================================
/**
* Execute _doVerify with a timeout. On timeout, returns warn-and-proceed.
* @param {object} context
* @returns {Promise<object>}
*/
async _executeWithTimeout(context) {
return new Promise((resolve, reject) => {
let isSettled = false;
const timer = setTimeout(() => {
if (!isSettled) {
isSettled = true;
this._log('warn', `Gate timed out after ${this._timeoutMs}ms. Warn-and-proceed.`);
resolve({
passed: true,
warnings: [`Gate ${this._gateId} timed out after ${this._timeoutMs}ms`],
opportunities: [],
});
}
}, this._timeoutMs);
this._doVerify(context)
.then((result) => {
if (!isSettled) {
isSettled = true;
clearTimeout(timer);
resolve(result);
}
})
.catch((error) => {
if (!isSettled) {
isSettled = true;
clearTimeout(timer);
reject(error);
}
});
});
}
/**
* Log a gate invocation for metrics.
* @param {object} result — GateResult structure
*/
_logInvocation(result) {
this._log('info', `Gate ${this._gateId} invoked`, {
passed: result.result.passed,
blocking: result.result.blocking,
warnings: result.result.warnings.length,
opportunities: result.result.opportunities.length,
executionMs: result.executionMs,
circuitBreakerState: result.circuitBreakerState,
invocationCount: this._invocationCount,
});
}
/**
* Internal logging helper.
* @param {string} level — 'info', 'warn', 'error'
* @param {string} message
* @param {object} [data]
*/
_log(level, message, data) {
const prefix = `[IDS-${this._gateId}]`;
const logFn = this._logger[level] || this._logger.log || console.log;
if (data) {
logFn(`${prefix} ${message}`, data);
} else {
logFn(`${prefix} ${message}`);
}
}
}
module.exports = {
VerificationGate,
createGateResult,
DEFAULT_TIMEOUT_MS,
};