diff --git a/AGENTS.md b/AGENTS.md index 12105c2..a34f781 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,7 @@ npm run mentor-eval ## Git Workflows -- **Base branch**: `main` or `migration/multi-repo-reorg` +- **Base branch**: `main` - **Branch naming**: `feat/`, `fix/`, `refactor/` - **Commit messages**: Conventional commits (e.g., `feat(validators): add PoInsight v2`) - **Before committing**: Run `npm test && npm run validate-all` diff --git a/analysts/froggy.enrichment_adapter.ts b/analysts/froggy.enrichment_adapter.ts index 4a0e164..76ef567 100644 --- a/analysts/froggy.enrichment_adapter.ts +++ b/analysts/froggy.enrichment_adapter.ts @@ -5,69 +5,6 @@ import type { FroggyTrendPullbackInput } from "./froggy.trend_pullback_v1.js"; -/** - * EnrichmentProfile - Configuration for which enrichment categories to apply - * - * This is the "enrichment design" that the enrichment designer (and similar personas) will configure. - * It specifies which enrichment categories should be enabled and how they should be parameterized. - * - * Categories map to DAG enrichment nodes: - * - technical: Technical indicators (EMA, RSI, volume, etc.) - * - pattern: Chart pattern detection (engulfing, hammer, etc.) - * - sentiment: Market sentiment analysis - * - news: News/event analysis - * - aiMl: AI/ML ensemble predictions - * - * Missing categories are treated as "use default behavior" (typically enabled with default preset). - * - * @example - * // Trend-pullback profile with all categories enabled - * const fullProfile: EnrichmentProfile = { - * technical: { enabled: true, preset: "trend_pullback" }, - * pattern: { enabled: true, preset: "reversal_patterns" }, - * sentiment: { enabled: true }, - * news: { enabled: true }, - * aiMl: { enabled: true, preset: "ensemble_v1" } - * }; - * - * @example - * // TA-only profile (no sentiment or news) - * const taOnlyProfile: EnrichmentProfile = { - * technical: { enabled: true, preset: "full_suite" }, - * pattern: { enabled: true }, - * sentiment: { enabled: false }, - * news: { enabled: false }, - * aiMl: { enabled: false } - * }; - */ -export interface EnrichmentProfile { - technical?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - pattern?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - sentiment?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - news?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - aiMl?: { - enabled: boolean; - preset?: string; - params?: Record; - }; -} - /** * FroggyAiMlV1 - AI/ML model predictions from Tiny Brains * diff --git a/docs/ENRICHMENT_PROFILE_SPEC.v0.1.md b/docs/ENRICHMENT_PROFILE_SPEC.v0.1.md deleted file mode 100644 index 2bf28a0..0000000 --- a/docs/ENRICHMENT_PROFILE_SPEC.v0.1.md +++ /dev/null @@ -1,279 +0,0 @@ -# EnrichmentProfile Specification v0.1 - -> ℹ️ Persona names below (e.g. "Pixel Rick") are illustrative of an enrichment-design role only. The legacy Froggy demo chain has been removed and the reactor is scored-only; `EnrichmentProfile` itself remains a valid configuration type. See `afi-reactor/src/config/froggyPipeline.ts`. - -## Overview - -**EnrichmentProfile** is a first-class configuration type that specifies which enrichment categories should be applied to a signal and how they should be parameterized. It serves as the "enrichment design" that personas like **Pixel Rick** (and similar engineering/configuration agents) will construct and attach to signals entering Froggy-style pipelines. - -This specification is **v0.1** and is designed to be non-breaking. Future versions can add fields but MUST preserve backwards compatibility where possible. - ---- - -## Purpose - -EnrichmentProfile enables: - -1. **Selective enrichment**: Only run enrichment categories that are relevant to a specific trading strategy -2. **Persona-driven design**: Personas like Pixel Rick can design enrichment strategies tailored to specific market conditions or signal types -3. **Resource optimization**: Avoid running expensive enrichment operations (e.g., sentiment analysis, news scraping) when they're not needed -4. **Testability**: Easily test pipelines with different enrichment configurations - ---- - -## TypeScript Definition - -```typescript -export interface EnrichmentProfile { - technical?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - pattern?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - sentiment?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - news?: { - enabled: boolean; - preset?: string; - params?: Record; - }; - aiMl?: { - enabled: boolean; - preset?: string; - params?: Record; - }; -} -``` - ---- - -## Enrichment Categories - -### 1. **technical** -Technical analysis indicators (EMA, RSI, volume, etc.) - -**Common presets**: -- `"default"`: Standard TA suite (EMA 20/50, RSI, volume) -- `"full_suite"`: Extended TA (includes MACD, Bollinger Bands, ATR, etc.) -- `"trend_pullback"`: Optimized for trend-pullback strategies (EMA distance, sweet spot detection) -- `"minimal"`: Only essential indicators (EMA 20, volume) - -### 2. **pattern** -Chart pattern detection (engulfing, hammer, liquidity sweeps, etc.) - -**Common presets**: -- `"default"`: Standard candlestick patterns -- `"reversal_patterns"`: Focus on reversal patterns (engulfing, hammer, morning star) -- `"continuation_patterns"`: Focus on continuation patterns (flags, pennants) -- `"liquidity_sweeps"`: Detect stop hunts and liquidity grabs - -### 3. **sentiment** -Market sentiment analysis (social media, on-chain metrics, etc.) - -**Common presets**: -- `"default"`: Balanced sentiment analysis -- `"social_heavy"`: Emphasize social media sentiment -- `"onchain_heavy"`: Emphasize on-chain metrics -- `"minimal"`: Basic sentiment score only - -### 4. **news** -News and event analysis (headlines, shock events, etc.) - -**Common presets**: -- `"default"`: Standard news analysis -- `"shock_events_only"`: Only detect major shock events -- `"crypto_native"`: Focus on crypto-specific news sources -- `"macro"`: Focus on macro economic news - -### 5. **aiMl** -AI/ML ensemble predictions - -**Common presets**: -- `"default"`: Standard ensemble -- `"ensemble_v1"`: First-generation ensemble model -- `"ensemble_v2"`: Second-generation ensemble model (if available) -- `"minimal"`: Single model prediction only - ---- - -## Example Profiles - -### Example 1: Trend Pullback (All Categories Enabled) - -```json -{ - "technical": { - "enabled": true, - "preset": "trend_pullback" - }, - "pattern": { - "enabled": true, - "preset": "reversal_patterns" - }, - "sentiment": { - "enabled": true, - "preset": "default" - }, - "news": { - "enabled": true, - "preset": "shock_events_only" - }, - "aiMl": { - "enabled": true, - "preset": "ensemble_v1" - } -} -``` - -**Use case**: Comprehensive analysis for high-conviction trend-pullback setups. Pixel Rick would design this profile for signals where all available data is valuable. - ---- - -### Example 2: TA-Only (No Sentiment or News) - -```json -{ - "technical": { - "enabled": true, - "preset": "full_suite" - }, - "pattern": { - "enabled": true, - "preset": "default" - }, - "sentiment": { - "enabled": false - }, - "news": { - "enabled": false - }, - "aiMl": { - "enabled": false - } -} -``` - -**Use case**: Pure technical analysis for strategies that ignore sentiment and news. Pixel Rick would design this profile for scalping or intraday strategies where fundamentals are less relevant. - ---- - -### Example 3: Sentiment-Heavy (Minimal TA) - -```json -{ - "technical": { - "enabled": true, - "preset": "minimal" - }, - "pattern": { - "enabled": false - }, - "sentiment": { - "enabled": true, - "preset": "social_heavy" - }, - "news": { - "enabled": true, - "preset": "crypto_native" - }, - "aiMl": { - "enabled": true, - "preset": "ensemble_v2" - } -} -``` - -**Use case**: Sentiment-driven strategies for meme coins or highly social assets. Pixel Rick would design this profile for signals where social momentum is the primary driver. - ---- - -## Usage in afi-reactor Pipelines - -### How Personas Attach Profiles - -Personas like **Pixel Rick** construct EnrichmentProfile objects and attach them to signals at ingestion time: - -```typescript -// Pixel Rick designs a profile for a specific strategy -const profile: EnrichmentProfile = { - technical: { enabled: true, preset: "trend_pullback" }, - pattern: { enabled: true, preset: "reversal_patterns" }, - sentiment: { enabled: false }, - news: { enabled: false }, - aiMl: { enabled: true, preset: "ensemble_v1" } -}; - -// Attach to signal draft -const signalDraft = { - symbol: "BTC/USDT", - timeframe: "1h", - strategy: "froggy_trend_pullback_v1", - enrichmentProfile: profile // <-- Pixel Rick's design -}; -``` - -### How afi-reactor Honors Profiles - -The `froggy-enrichment-adapter` plugin reads the profile from `signal.meta.enrichmentProfile` and: - -1. Only populates enrichment sections where `enabled !== false` -2. Uses the specified `preset` to configure enrichment behavior -3. Falls back to a default profile (all categories enabled) if no profile is provided - ---- - -## Contract for Personas - -**Personas like Pixel Rick** are responsible for: - -1. **Analyzing the signal strategy** and determining which enrichment categories are relevant -2. **Constructing an appropriate EnrichmentProfile** with enabled/disabled categories and presets -3. **Attaching the profile to the signal** at ingestion time (via `enrichmentProfile` field) - -**afi-reactor** is responsible for: - -1. **Preserving the profile** through the pipeline (Alpha Scout → Pixel Rick → Froggy Enrichment) -2. **Honoring the profile** in enrichment nodes (only run enabled categories) -3. **Tracking enrichment metadata** (which categories were actually enriched) - ---- - -## Backwards Compatibility - -- **Missing categories** default to enabled (for backwards compatibility with signals that don't specify a profile) -- **Missing profile** defaults to all categories enabled with "default" preset -- **Future versions** can add new categories or fields, but MUST NOT break existing profiles - ---- - -## Future Enhancements (Not in v0.1) - -- **Preset validation**: Validate that specified presets exist and are supported -- **Params schema**: Define schemas for category-specific params -- **Profile templates**: Pre-defined profiles for common strategies (stored in afi-config repo) -- **Profile versioning**: Track which version of EnrichmentProfile was used for a signal -- **Dynamic profiles**: Personas can adjust profiles based on market conditions - ---- - -## Related Documentation - -- `afi-core/analysts/froggy.enrichment_adapter.ts` - EnrichmentProfile type definition -- `afi-reactor/plugins/froggy-enrichment-adapter.plugin.ts` - Profile implementation -- `afi-reactor/test/froggyPipeline.test.ts` - Profile behavior tests - ---- - -**Version**: 0.1 -**Status**: Active -**Maintained by**: AFI Protocol Core Team -**Last updated**: 2025-12-06 - diff --git a/runtime/types.ts b/runtime/types.ts index a0f9069..51e5b7b 100644 --- a/runtime/types.ts +++ b/runtime/types.ts @@ -112,14 +112,3 @@ export interface MinimalSignalPayload { /** Arbitrary metadata */ metadata?: Record; } - -// ============================================================================ -// Re-exports for convenience -// ============================================================================ - -/** - * Type alias for backward compatibility. - * Use SignalPayload directly in new code. - * @deprecated Use SignalPayload instead - */ -export type Signal = SignalPayload; diff --git a/schemas/universal_signal_schema.backup.ts b/schemas/universal_signal_schema.backup.ts deleted file mode 100644 index 26e9c8a..0000000 --- a/schemas/universal_signal_schema.backup.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { z } from "zod"; - -/** - * LEGACY STAGED PIPELINE SCHEMA (ARCHIVED) - * ---------------------------------------- - * This file captures an older "all-in-one" view of the signal lifecycle - * (RAW → ENRICHED → ANALYZED → SCORED) and an embedded PipelineConfigSchema. - * - * It is kept only as a design artifact. - * DO NOT use this file for new runtime behavior. - * DO NOT export anything from here via schemas/index.ts. - * - * Canonical v0.1 schemas now live in: - * - schemas/universal_signal_schema.ts (SignalSchema) - * - schemas/pipeline_config_schema.ts (PipelineConfigSchema) - * - afi-infra/schemas/*.ts (TSSD + enrichment) - */ - -/* ---------- ENUMS ---------- */ -export const SignalActionSchema = z.enum(["buy", "sell"]); -export const SignalStrengthSchema = z.enum(["low", "medium", "high", "very-high"]); -export const SignalTimeframeSchema = z.enum(["1m","5m","15m","30m","1h","4h","1d","1w"]); -export const SignalSourceSchema = z.enum(["manual","bot","tradingview"]); -export const MarketSchema = z.enum(["crypto","forex","stocks","commodities","futures"]); -export const IndicatorNameSchema = z.enum([ - "RSI","MACD","Moving Average","Bollinger Bands","OBV","ATR","Stochastic","Volume" -]); - -/* ---------- COMPONENTS ---------- */ -export const SignalIndicatorSchema = z.object({ - name: IndicatorNameSchema, - value: z.union([z.number(), z.string()]), - timeframe: SignalTimeframeSchema.optional(), -}); - -export const SignalAnalysisSchema = z.object({ - type: z.string(), - result: z.union([z.string(), z.number()]), - confidence: z.number().min(0).max(100).optional(), -}); - -export const PatternAnalysisSchema = z.object({ - pattern: z.string(), - confidence: z.number().min(0).max(100), - description: z.string().optional(), -}); - -export const FuturesContractDataSchema = z.object({ - contractSize: z.number().positive(), - leverage: z.number().positive(), - marginType: z.enum(["isolated","cross"]), - expiryDate: z.string().datetime().optional(), -}); - -/* ---------- BASE ---------- */ -export const BaseSignalSchema = z.object({ - id: z.string(), - symbol: z.string(), - market: MarketSchema, - action: SignalActionSchema, - price: z.number().positive(), - timestamp: z.number().int().positive(), - source: SignalSourceSchema, -}); - -/* ---------- COMPLETE ---------- */ -export const SignalSchema = BaseSignalSchema.extend({ - targetPrice: z.number().positive(), - stopLoss: z.number().positive(), - timeframe: SignalTimeframeSchema, - strength: SignalStrengthSchema, - indicators: z.array(SignalIndicatorSchema), - analysis: z.array(SignalAnalysisSchema), - patternAnalysis: PatternAnalysisSchema.optional(), - takeProfitLevels: z.array(z.number().positive()).optional(), - riskRewardRatio: z.number().positive().optional(), - futuresData: FuturesContractDataSchema.optional(), - score: z.number().min(0).max(100), - notes: z.string().optional(), - userId: z.string().optional(), - subscribed: z.boolean(), -}); - -/* ---------- PIPELINE VARIANTS ---------- */ -export const IndicatorDataSchema = z.object({ - name: z.string(), - value: z.union([z.number(), z.array(z.number()), z.record(z.string(), z.number())]), - timeframe: z.string(), - timestamp: z.number().int().positive(), - meta: z.record(z.string(), z.any()).optional(), -}); - -export const RawSignalSchema = BaseSignalSchema.extend({ - timeframe: SignalTimeframeSchema.optional(), - pattern: z.string().optional(), - status: z.literal("new"), -}); - -export const EnrichedSignalSchema = BaseSignalSchema.extend({ - timeframe: SignalTimeframeSchema, - pattern: z.string().optional(), - indicators: z.record(z.string(), IndicatorDataSchema), - status: z.literal("enriched"), -}); - -export const AnalyzedSignalSchema = EnrichedSignalSchema.extend({ - analysis: z.object({ - patternConfidence: z.number().min(0).max(100).optional(), - trendStrength: z.number().min(0).max(100).optional(), - supportLevel: z.number().positive().optional(), - resistanceLevel: z.number().positive().optional(), - volumeAnalysis: z.string().optional(), - riskLevel: z.enum(["low","medium","high"]).optional(), - comments: z.string().optional(), - }), - status: z.literal("analyzed"), -}); - -export const ScoredSignalSchema = AnalyzedSignalSchema.extend({ - score: z.object({ - overall: z.number().min(0).max(100), - technical: z.number().min(0).max(100), - fundamental: z.number().min(0).max(100).optional(), - sentiment: z.number().min(0).max(100).optional(), - breakdown: z.record(z.string(), z.number()), - }), - status: z.literal("scored"), -}); - -/* ---------- CONFIG ---------- */ -export const PipelineConfigSchema = z.object({ - enabled: z.boolean(), - enrichment: z.object({ - providers: z.array(z.string()), - indicators: z.array(z.string()), - timeframes: z.array(z.string()), - }), - analysis: z.object({ - providers: z.array(z.string()), - methods: z.array(z.string()), - }), - scoring: z.object({ - providers: z.array(z.string()), - weights: z.record(z.string(), z.number()), - threshold: z.number().min(0).max(100), - }), -}); - -/* ---------- EXPORTS ---------- */ -export { - SignalSchema, - BaseSignalSchema, - RawSignalSchema, - EnrichedSignalSchema, - AnalyzedSignalSchema, - ScoredSignalSchema, - PipelineConfigSchema, -}; diff --git a/src/dag/SignalEnvelope.ts b/src/dag/SignalEnvelope.ts deleted file mode 100644 index 962d97b..0000000 --- a/src/dag/SignalEnvelope.ts +++ /dev/null @@ -1,434 +0,0 @@ -/** - * AFI Core - Signal Envelope - * - * This file defines TypeScript interfaces for signal envelope, - * extending the core signal schema with DAG-specific metadata. - * - * The signal envelope wraps raw signals with enrichment results and execution - * metadata, providing a complete record of the signal processing pipeline. - * - * @module afi-core/src/dag/SignalEnvelope - */ - -/** - * Signal envelope - * - * Wraps a raw signal with enrichment results and execution metadata. - * The envelope provides a complete record of the signal processing pipeline, - * including which enrichment nodes were executed, which were skipped, and - * the full execution trace. - */ -export interface SignalEnvelope { - /** Signal ID. Unique identifier for the signal. */ - signalId: string; - - /** Raw signal data. The original signal before enrichment. */ - rawSignal: unknown; - - /** Enrichment results. Map of node ID to enrichment result. */ - enrichmentResults: Map; - - /** Analyst configuration ID. The ID of the analyst configuration used for processing. */ - analystConfigId: string; - - /** Envelope metadata. Tracks creation, updates, and execution details. */ - metadata: { - /** Creation timestamp. ISO 8601 timestamp when the envelope was created. */ - createdAt: string; - - /** Last update timestamp. ISO 8601 timestamp when the envelope was last updated. */ - updatedAt: string; - - /** Enrichment nodes executed. Array of node IDs that were successfully executed. */ - enrichmentNodesExecuted: string[]; - - /** Enrichment nodes skipped. Array of node IDs that were skipped (e.g., disabled, optional failure). */ - enrichmentNodesSkipped: string[]; - - /** Execution trace. Array of trace entries for each executed node. */ - executionTrace: ExecutionTraceEntry[]; - }; -} - -/** - * Execution trace entry - * - * Represents a single entry in the execution trace. Each node execution - * produces a trace entry with timing and status information. - */ -export interface ExecutionTraceEntry { - /** Node ID. */ - nodeId: string; - - /** Node type. */ - nodeType: 'required' | 'enrichment' | 'ingress'; - - /** Start time. ISO 8601 timestamp. */ - startTime: string; - - /** End time. ISO 8601 timestamp. Present only after node completes. */ - endTime?: string; - - /** Duration in milliseconds. Present only after node completes. */ - duration?: number; - - /** Status. */ - status: 'pending' | 'running' | 'completed' | 'failed'; - - /** Error message. Present only if node failed. */ - error?: string; -} - -/** - * Enrichment result - * - * Represents the result of a single enrichment node execution. - */ -export interface EnrichmentResult { - /** Node ID. */ - nodeId: string; - - /** Result data. The actual enrichment data produced by the node. */ - data: unknown; - - /** Result metadata. Execution metadata for the enrichment result. */ - metadata: { - /** Execution time in milliseconds. */ - executionTime: number; - - /** Success flag. */ - success: boolean; - - /** Error message. Present only if execution failed. */ - error?: string; - }; -} - -/** - * Signal envelope status - * - * Represents the overall status of a signal envelope. - */ -export type SignalEnvelopeStatus = 'pending' | 'processing' | 'completed' | 'failed'; - -/** - * Signal envelope summary - * - * Provides a summary of a signal envelope for quick inspection. - */ -export interface SignalEnvelopeSummary { - /** Signal ID. */ - signalId: string; - - /** Analyst configuration ID. */ - analystConfigId: string; - - /** Envelope status. */ - status: SignalEnvelopeStatus; - - /** Number of enrichment nodes executed. */ - nodesExecuted: number; - - /** Number of enrichment nodes skipped. */ - nodesSkipped: number; - - /** Number of enrichment nodes failed. */ - nodesFailed: number; - - /** Total execution time in milliseconds. */ - totalExecutionTime: number; - - /** Creation timestamp. */ - createdAt: string; - - /** Last update timestamp. */ - updatedAt: string; -} - -/** - * Enrichment node status - * - * Represents the status of a single enrichment node. - */ -export type EnrichmentNodeStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; - -/** - * Enrichment node summary - * - * Provides a summary of a single enrichment node execution. - */ -export interface EnrichmentNodeSummary { - /** Node ID. */ - nodeId: string; - - /** Node type. */ - nodeType: 'required' | 'enrichment' | 'ingress'; - - /** Node status. */ - status: EnrichmentNodeStatus; - - /** Execution time in milliseconds. Present only if node was executed. */ - executionTime?: number; - - /** Error message. Present only if node failed. */ - error?: string; -} - -/** - * Signal envelope validation result - * - * Result of validating a signal envelope. - */ -export interface SignalEnvelopeValidationResult { - /** Whether the envelope is valid. */ - valid: boolean; - - /** Validation errors. */ - errors: string[]; - - /** Validation warnings. */ - warnings: string[]; -} - -/** - * Type guard to check if an object is a SignalEnvelope - */ -export function isSignalEnvelope(obj: unknown): obj is SignalEnvelope { - if (typeof obj !== 'object' || obj === null) { - return false; - } - - const envelope = obj as unknown as Record; - const metadata = envelope.metadata as unknown as Record | undefined; - - return ( - typeof envelope.signalId === 'string' && - envelope.rawSignal !== undefined && - envelope.enrichmentResults instanceof Map && - typeof envelope.analystConfigId === 'string' && - typeof metadata === 'object' && - metadata !== null && - typeof metadata.createdAt === 'string' && - typeof metadata.updatedAt === 'string' && - Array.isArray(metadata.enrichmentNodesExecuted) && - Array.isArray(metadata.enrichmentNodesSkipped) && - Array.isArray(metadata.executionTrace) - ); -} - -/** - * Type guard to check if an object is an ExecutionTraceEntry - */ -export function isExecutionTraceEntry(obj: unknown): obj is ExecutionTraceEntry { - if (typeof obj !== 'object' || obj === null) { - return false; - } - - const entry = obj as unknown as Record; - - return ( - typeof entry.nodeId === 'string' && - (entry.nodeType === 'required' || entry.nodeType === 'enrichment' || entry.nodeType === 'ingress') && - typeof entry.startTime === 'string' && - (entry.status === 'pending' || entry.status === 'running' || entry.status === 'completed' || entry.status === 'failed') - ); -} - -/** - * Type guard to check if an object is an EnrichmentResult - */ -export function isEnrichmentResult(obj: unknown): obj is EnrichmentResult { - if (typeof obj !== 'object' || obj === null) { - return false; - } - - const result = obj as unknown as Record; - const metadata = result.metadata as unknown as Record | undefined; - - return ( - typeof result.nodeId === 'string' && - result.data !== undefined && - typeof metadata === 'object' && - metadata !== null && - typeof metadata.executionTime === 'number' && - typeof metadata.success === 'boolean' - ); -} - -/** - * Create a new signal envelope - */ -export function createSignalEnvelope( - signalId: string, - rawSignal: unknown, - analystConfigId: string -): SignalEnvelope { - const now = new Date().toISOString(); - - return { - signalId, - rawSignal, - enrichmentResults: new Map(), - analystConfigId, - metadata: { - createdAt: now, - updatedAt: now, - enrichmentNodesExecuted: [], - enrichmentNodesSkipped: [], - executionTrace: [], - }, - }; -} - -/** - * Add enrichment result to signal envelope - */ -export function addEnrichmentResult( - envelope: SignalEnvelope, - nodeId: string, - result: unknown, - executionTime: number, - success: boolean, - error?: string -): SignalEnvelope { - const updatedEnvelope = { ...envelope }; - - // Add enrichment result - updatedEnvelope.enrichmentResults = new Map(envelope.enrichmentResults); - updatedEnvelope.enrichmentResults.set(nodeId, { - nodeId, - data: result, - metadata: { - executionTime, - success, - error, - }, - }); - - // Update metadata - updatedEnvelope.metadata = { ...envelope.metadata }; - updatedEnvelope.metadata.updatedAt = new Date().toISOString(); - - if (success) { - updatedEnvelope.metadata.enrichmentNodesExecuted = [ - ...envelope.metadata.enrichmentNodesExecuted, - nodeId, - ]; - } else { - updatedEnvelope.metadata.enrichmentNodesSkipped = [ - ...envelope.metadata.enrichmentNodesSkipped, - nodeId, - ]; - } - - return updatedEnvelope; -} - -/** - * Add execution trace entry to signal envelope - */ -export function addExecutionTraceEntry( - envelope: SignalEnvelope, - entry: ExecutionTraceEntry -): SignalEnvelope { - const updatedEnvelope = { ...envelope }; - - updatedEnvelope.metadata = { ...envelope.metadata }; - updatedEnvelope.metadata.executionTrace = [ - ...envelope.metadata.executionTrace, - entry, - ]; - updatedEnvelope.metadata.updatedAt = new Date().toISOString(); - - return updatedEnvelope; -} - -/** - * Get signal envelope summary - */ -export function getSignalEnvelopeSummary( - envelope: SignalEnvelope -): SignalEnvelopeSummary { - const nodesFailed = envelope.metadata.executionTrace.filter( - entry => entry.status === 'failed' - ).length; - - const totalExecutionTime = envelope.metadata.executionTrace.reduce( - (sum, entry) => sum + (entry.duration || 0), - 0 - ); - - return { - signalId: envelope.signalId, - analystConfigId: envelope.analystConfigId, - status: nodesFailed > 0 ? 'failed' : 'completed', - nodesExecuted: envelope.metadata.enrichmentNodesExecuted.length, - nodesSkipped: envelope.metadata.enrichmentNodesSkipped.length, - nodesFailed, - totalExecutionTime, - createdAt: envelope.metadata.createdAt, - updatedAt: envelope.metadata.updatedAt, - }; -} - -/** - * Validate signal envelope - */ -export function validateSignalEnvelope( - envelope: SignalEnvelope -): SignalEnvelopeValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - // Check required fields - if (!envelope.signalId) { - errors.push('Missing signalId'); - } - - if (!envelope.analystConfigId) { - errors.push('Missing analystConfigId'); - } - - if (!envelope.metadata) { - errors.push('Missing metadata'); - } else { - if (!envelope.metadata.createdAt) { - errors.push('Missing metadata.createdAt'); - } - - if (!envelope.metadata.updatedAt) { - errors.push('Missing metadata.updatedAt'); - } - - if (!Array.isArray(envelope.metadata.enrichmentNodesExecuted)) { - errors.push('Missing or invalid metadata.enrichmentNodesExecuted'); - } - - if (!Array.isArray(envelope.metadata.enrichmentNodesSkipped)) { - errors.push('Missing or invalid metadata.enrichmentNodesSkipped'); - } - - if (!Array.isArray(envelope.metadata.executionTrace)) { - errors.push('Missing or invalid metadata.executionTrace'); - } - } - - // Check for duplicate trace entries - const nodeIds = envelope.metadata.executionTrace.map(entry => entry.nodeId); - const duplicateNodeIds = nodeIds.filter((id, index) => nodeIds.indexOf(id) !== index); - if (duplicateNodeIds.length > 0) { - warnings.push(`Duplicate trace entries for nodes: ${duplicateNodeIds.join(', ')}`); - } - - // Check for failed nodes - const failedNodes = envelope.metadata.executionTrace.filter(entry => entry.status === 'failed'); - if (failedNodes.length > 0) { - warnings.push(`Failed nodes: ${failedNodes.map(entry => entry.nodeId).join(', ')}`); - } - - return { - valid: errors.length === 0, - errors, - warnings, - }; -} diff --git a/test/guardrails/no-legacy-dag-path.test.ts b/test/guardrails/no-legacy-dag-path.test.ts new file mode 100644 index 0000000..de66be5 --- /dev/null +++ b/test/guardrails/no-legacy-dag-path.test.ts @@ -0,0 +1,17 @@ +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import { describe, it, expect } from "vitest"; + +/** + * Forward-only guardrail: the obsolete `src/dag/` orchestration path must never + * return to afi-core. The DAG-node model was retired in favor of the + * manifest-driven five-lane GraphExecutor runtime; `src/dag/` is a banned path. + */ +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +describe("no-legacy-dag-path guardrail", () => { + it("src/dag/ does not exist in afi-core", () => { + expect(existsSync(resolve(REPO_ROOT, "src/dag"))).toBe(false); + }); +});