All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m19s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m5s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m26s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
257 lines
8.8 KiB
TypeScript
257 lines
8.8 KiB
TypeScript
// ============================================================================
|
||
// Pipeline Orchestrator
|
||
// Runs all steps sequentially, tracks state, supports re-running individual steps.
|
||
// ============================================================================
|
||
|
||
import * as fs from "node:fs/promises";
|
||
import * as path from "node:path";
|
||
import { validateEstimation } from "./validators.js";
|
||
import { executeSynthesize } from "./steps/05-synthesize.js";
|
||
import { executeCritique } from "./steps/06-critique.js";
|
||
import type {
|
||
PipelineConfig,
|
||
PipelineInput,
|
||
EstimationState,
|
||
StepResult,
|
||
} from "./types.js";
|
||
|
||
export interface PipelineCallbacks {
|
||
onStepStart?: (stepId: string, stepName: string) => void;
|
||
onStepComplete?: (stepId: string, result: StepResult) => void;
|
||
onStepError?: (stepId: string, error: string) => void;
|
||
}
|
||
|
||
/**
|
||
* The main estimation pipeline orchestrator.
|
||
* Runs steps sequentially, persists state between steps, supports re-entry.
|
||
*/
|
||
export class EstimationPipeline {
|
||
private config: PipelineConfig;
|
||
private state: EstimationState;
|
||
private callbacks: PipelineCallbacks;
|
||
|
||
constructor(config: PipelineConfig, callbacks: PipelineCallbacks = {}) {
|
||
this.config = config;
|
||
this.callbacks = callbacks;
|
||
this.state = this.createInitialState();
|
||
}
|
||
|
||
private createInitialState(): EstimationState {
|
||
return {
|
||
concept: null as any, // Will be set in run()
|
||
usage: {
|
||
totalPromptTokens: 0,
|
||
totalCompletionTokens: 0,
|
||
totalCost: 0,
|
||
perStep: [],
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Run the full estimation pipeline from a completed project concept.
|
||
*/
|
||
async run(input: PipelineInput): Promise<EstimationState> {
|
||
this.state.concept = input.concept;
|
||
this.state.budget = input.budget;
|
||
|
||
// Ensure output directories
|
||
await fs.mkdir(this.config.outputDir, { recursive: true });
|
||
|
||
// Step 5: Position synthesis
|
||
await this.runStep("05-synthesize", "Position Descriptions", async () => {
|
||
const result = await executeSynthesize(this.state, this.config);
|
||
if (result.success) this.state.positionDescriptions = result.data;
|
||
return result;
|
||
});
|
||
|
||
// Step 6: Quality critique
|
||
await this.runStep(
|
||
"06-critique",
|
||
"Quality Gate (Industrial Critic)",
|
||
async () => {
|
||
const result = await executeCritique(this.state, this.config);
|
||
if (result.success) {
|
||
this.state.critiquePassed = result.data.passed;
|
||
this.state.critiqueErrors =
|
||
result.data.errors?.map((e: any) => `${e.field}: ${e.issue}`) || [];
|
||
|
||
// Apply corrections
|
||
if (result.data.corrections) {
|
||
const corrections = result.data.corrections;
|
||
// Note: We only correct the positionDescriptions since briefing/design/sitemap are locked in the concept phase.
|
||
// If the critique suggests changes to those, it should be a warning or failure.
|
||
if (corrections.positionDescriptions) {
|
||
this.state.positionDescriptions = {
|
||
...this.state.positionDescriptions,
|
||
...corrections.positionDescriptions,
|
||
};
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
},
|
||
);
|
||
|
||
// Step 7: Deterministic validation
|
||
await this.runStep("07-validate", "Deterministic Validation", async () => {
|
||
// Build the merged form state first
|
||
this.state.formState = this.buildFormState();
|
||
const validationResult = validateEstimation(this.state);
|
||
this.state.validationResult = validationResult;
|
||
|
||
if (!validationResult.passed) {
|
||
console.log("\n⚠️ Validation Issues:");
|
||
for (const error of validationResult.errors) {
|
||
console.log(` ❌ [${error.code}] ${error.message}`);
|
||
}
|
||
}
|
||
if (validationResult.warnings.length > 0) {
|
||
console.log("\n⚡ Warnings:");
|
||
for (const warning of validationResult.warnings) {
|
||
console.log(` ⚡ [${warning.code}] ${warning.message}`);
|
||
if (warning.suggestion) console.log(` → ${warning.suggestion}`);
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
data: validationResult,
|
||
usage: {
|
||
step: "07-validate",
|
||
model: "none",
|
||
promptTokens: 0,
|
||
completionTokens: 0,
|
||
cost: 0,
|
||
durationMs: 0,
|
||
},
|
||
};
|
||
});
|
||
|
||
// Save final state
|
||
await this.saveState();
|
||
|
||
return this.state;
|
||
}
|
||
|
||
/**
|
||
* Run a single step with callbacks and error handling.
|
||
*/
|
||
private async runStep(
|
||
stepId: string,
|
||
stepName: string,
|
||
executor: () => Promise<StepResult>,
|
||
): Promise<void> {
|
||
this.callbacks.onStepStart?.(stepId, stepName);
|
||
console.log(`\n📍 ${stepName}...`);
|
||
|
||
try {
|
||
const result = await executor();
|
||
if (result.usage) {
|
||
this.state.usage.perStep.push(result.usage);
|
||
this.state.usage.totalPromptTokens += result.usage.promptTokens;
|
||
this.state.usage.totalCompletionTokens += result.usage.completionTokens;
|
||
this.state.usage.totalCost += result.usage.cost;
|
||
}
|
||
|
||
if (result.success) {
|
||
const cost = result.usage?.cost
|
||
? ` ($${result.usage.cost.toFixed(4)})`
|
||
: "";
|
||
const duration = result.usage?.durationMs
|
||
? ` [${(result.usage.durationMs / 1000).toFixed(1)}s]`
|
||
: "";
|
||
console.log(` ✅ ${stepName} complete${cost}${duration}`);
|
||
this.callbacks.onStepComplete?.(stepId, result);
|
||
} else {
|
||
console.error(` ❌ ${stepName} failed: ${result.error}`);
|
||
this.callbacks.onStepError?.(stepId, result.error || "Unknown error");
|
||
throw new Error(result.error);
|
||
}
|
||
} catch (err) {
|
||
const errorMsg = (err as Error).message;
|
||
this.callbacks.onStepError?.(stepId, errorMsg);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Build the final FormState compatible with @mintel/pdf.
|
||
*/
|
||
private buildFormState(): Record<string, any> {
|
||
const facts = this.state.concept.auditedFacts || {};
|
||
return {
|
||
projectType: "website",
|
||
...facts,
|
||
briefingSummary: this.state.concept.strategy.briefingSummary || "",
|
||
designVision: this.state.concept.strategy.designVision || "",
|
||
sitemap: this.state.concept.architecture.sitemap || [],
|
||
positionDescriptions: this.state.positionDescriptions || {},
|
||
websiteTopic:
|
||
this.state.concept.architecture.websiteTopic ||
|
||
facts.websiteTopic ||
|
||
"",
|
||
statusQuo: facts.isRelaunch ? "Relaunch" : "Neuentwicklung",
|
||
name: facts.personName || "",
|
||
email: facts.email || "",
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Save the full state to disk for later re-use.
|
||
*/
|
||
private async saveState(): Promise<void> {
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||
const companyName =
|
||
this.state.concept.auditedFacts?.companyName || "unknown";
|
||
|
||
// Save full state
|
||
const stateDir = path.join(this.config.outputDir, "json");
|
||
await fs.mkdir(stateDir, { recursive: true });
|
||
|
||
const statePath = path.join(stateDir, `${companyName}_${timestamp}.json`);
|
||
await fs.writeFile(
|
||
statePath,
|
||
JSON.stringify(this.state.formState, null, 2),
|
||
);
|
||
console.log(`\n📦 Saved state to: ${statePath}`);
|
||
|
||
// Save full pipeline state (for debugging / re-entry)
|
||
const debugPath = path.join(
|
||
stateDir,
|
||
`${companyName}_${timestamp}_debug.json`,
|
||
);
|
||
await fs.writeFile(debugPath, JSON.stringify(this.state, null, 2));
|
||
|
||
// Print usage summary
|
||
console.log("\n──────────────────────────────────────────────");
|
||
console.log("📊 PIPELINE USAGE SUMMARY");
|
||
console.log("──────────────────────────────────────────────");
|
||
for (const step of this.state.usage.perStep) {
|
||
if (step.cost > 0) {
|
||
console.log(
|
||
` ${step.step}: ${step.model} — $${step.cost.toFixed(6)} (${(step.durationMs / 1000).toFixed(1)}s)`,
|
||
);
|
||
}
|
||
}
|
||
console.log("──────────────────────────────────────────────");
|
||
console.log(` TOTAL: $${this.state.usage.totalCost.toFixed(6)}`);
|
||
console.log(
|
||
` Tokens: ${(this.state.usage.totalPromptTokens + this.state.usage.totalCompletionTokens).toLocaleString()}`,
|
||
);
|
||
console.log("──────────────────────────────────────────────\n");
|
||
}
|
||
|
||
/** Get the current state (for CLI inspection). */
|
||
getState(): EstimationState {
|
||
return this.state;
|
||
}
|
||
|
||
/** Load a saved state from JSON. */
|
||
async loadState(jsonPath: string): Promise<void> {
|
||
const raw = await fs.readFile(jsonPath, "utf8");
|
||
const formState = JSON.parse(raw);
|
||
this.state.formState = formState;
|
||
}
|
||
}
|