feat: migrate npm registry from Verdaccio to Gitea Packages
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 35s
Monorepo Pipeline / 🧪 Test (push) Failing after 35s
Monorepo Pipeline / 🏗️ Build (push) Failing after 12s
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
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 35s
Monorepo Pipeline / 🧪 Test (push) Failing after 35s
Monorepo Pipeline / 🏗️ Build (push) Failing after 12s
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
This commit is contained in:
228
packages/estimation-engine/src/pipeline.ts
Normal file
228
packages/estimation-engine/src/pipeline.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
// ============================================================================
|
||||
// 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 { existsSync } from "node:fs";
|
||||
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,
|
||||
StepUsage,
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user