@studio-foundation/engine 0.3.0-beta.1 → 0.3.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/engine.conditions-group.test.d.ts +2 -0
- package/dist/__tests__/engine.conditions-group.test.d.ts.map +1 -0
- package/dist/__tests__/engine.conditions-group.test.js +222 -0
- package/dist/__tests__/engine.conditions-group.test.js.map +1 -0
- package/dist/__tests__/engine.conditions.test.d.ts +2 -0
- package/dist/__tests__/engine.conditions.test.d.ts.map +1 -0
- package/dist/__tests__/engine.conditions.test.js +164 -0
- package/dist/__tests__/engine.conditions.test.js.map +1 -0
- package/dist/__tests__/engine.resume.test.d.ts +2 -0
- package/dist/__tests__/engine.resume.test.d.ts.map +1 -0
- package/dist/__tests__/engine.resume.test.js +93 -0
- package/dist/__tests__/engine.resume.test.js.map +1 -0
- package/dist/__tests__/engine.script-stage.test.d.ts +2 -0
- package/dist/__tests__/engine.script-stage.test.d.ts.map +1 -0
- package/dist/__tests__/engine.script-stage.test.js +108 -0
- package/dist/__tests__/engine.script-stage.test.js.map +1 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.d.ts.map +1 -0
- package/{src/db/client.ts → dist/db/client.js} +1 -2
- package/dist/db/client.js.map +1 -0
- package/dist/engine.context-event.test.d.ts +2 -0
- package/dist/engine.context-event.test.d.ts.map +1 -0
- package/dist/engine.context-event.test.js +111 -0
- package/dist/engine.context-event.test.js.map +1 -0
- package/dist/engine.d.ts +52 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +364 -0
- package/dist/engine.js.map +1 -0
- package/dist/events.d.ts +163 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +3 -45
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/agent-loader.d.ts +4 -0
- package/dist/pipeline/agent-loader.d.ts.map +1 -0
- package/dist/pipeline/agent-loader.js +28 -0
- package/dist/pipeline/agent-loader.js.map +1 -0
- package/dist/pipeline/agent-loader.test.d.ts +2 -0
- package/dist/pipeline/agent-loader.test.d.ts.map +1 -0
- package/dist/pipeline/agent-loader.test.js +134 -0
- package/dist/pipeline/agent-loader.test.js.map +1 -0
- package/dist/pipeline/condition-evaluator.d.ts +6 -0
- package/dist/pipeline/condition-evaluator.d.ts.map +1 -0
- package/dist/pipeline/condition-evaluator.js +105 -0
- package/dist/pipeline/condition-evaluator.js.map +1 -0
- package/dist/pipeline/condition-evaluator.test.d.ts +2 -0
- package/dist/pipeline/condition-evaluator.test.d.ts.map +1 -0
- package/dist/pipeline/condition-evaluator.test.js +103 -0
- package/dist/pipeline/condition-evaluator.test.js.map +1 -0
- package/dist/pipeline/context-pack-loader.d.ts +3 -0
- package/dist/pipeline/context-pack-loader.d.ts.map +1 -0
- package/dist/pipeline/context-pack-loader.js +51 -0
- package/dist/pipeline/context-pack-loader.js.map +1 -0
- package/dist/pipeline/context-propagation.d.ts +28 -0
- package/dist/pipeline/context-propagation.d.ts.map +1 -0
- package/dist/pipeline/context-propagation.js +161 -0
- package/dist/pipeline/context-propagation.js.map +1 -0
- package/dist/pipeline/context-propagation.test.d.ts +2 -0
- package/dist/pipeline/context-propagation.test.d.ts.map +1 -0
- package/dist/pipeline/context-propagation.test.js +194 -0
- package/dist/pipeline/context-propagation.test.js.map +1 -0
- package/dist/pipeline/contract-loader.d.ts +4 -0
- package/dist/pipeline/contract-loader.d.ts.map +1 -0
- package/dist/pipeline/contract-loader.js +30 -0
- package/dist/pipeline/contract-loader.js.map +1 -0
- package/dist/pipeline/group-orchestrator.d.ts +19 -0
- package/dist/pipeline/group-orchestrator.d.ts.map +1 -0
- package/dist/pipeline/group-orchestrator.js +376 -0
- package/dist/pipeline/group-orchestrator.js.map +1 -0
- package/dist/pipeline/hook-executor.d.ts +28 -0
- package/dist/pipeline/hook-executor.d.ts.map +1 -0
- package/dist/pipeline/hook-executor.js +63 -0
- package/dist/pipeline/hook-executor.js.map +1 -0
- package/dist/pipeline/hook-executor.test.d.ts +2 -0
- package/dist/pipeline/hook-executor.test.d.ts.map +1 -0
- package/dist/pipeline/hook-executor.test.js +71 -0
- package/dist/pipeline/hook-executor.test.js.map +1 -0
- package/dist/pipeline/invariants-loader.d.ts +6 -0
- package/dist/pipeline/invariants-loader.d.ts.map +1 -0
- package/dist/pipeline/invariants-loader.js +15 -0
- package/dist/pipeline/invariants-loader.js.map +1 -0
- package/dist/pipeline/invariants-loader.test.d.ts +2 -0
- package/dist/pipeline/invariants-loader.test.d.ts.map +1 -0
- package/dist/pipeline/invariants-loader.test.js +39 -0
- package/dist/pipeline/invariants-loader.test.js.map +1 -0
- package/dist/pipeline/loader.d.ts +5 -0
- package/dist/pipeline/loader.d.ts.map +1 -0
- package/dist/pipeline/loader.js +132 -0
- package/dist/pipeline/loader.js.map +1 -0
- package/dist/pipeline/loader.test.d.ts +2 -0
- package/dist/pipeline/loader.test.d.ts.map +1 -0
- package/dist/pipeline/loader.test.js +120 -0
- package/dist/pipeline/loader.test.js.map +1 -0
- package/dist/pipeline/output-validator.d.ts +10 -0
- package/dist/pipeline/output-validator.d.ts.map +1 -0
- package/dist/pipeline/output-validator.js +20 -0
- package/dist/pipeline/output-validator.js.map +1 -0
- package/dist/pipeline/output-validator.test.d.ts +2 -0
- package/dist/pipeline/output-validator.test.d.ts.map +1 -0
- package/dist/pipeline/output-validator.test.js +77 -0
- package/dist/pipeline/output-validator.test.js.map +1 -0
- package/dist/pipeline/post-validator.d.ts +8 -0
- package/dist/pipeline/post-validator.d.ts.map +1 -0
- package/dist/pipeline/post-validator.js +103 -0
- package/dist/pipeline/post-validator.js.map +1 -0
- package/dist/pipeline/skill-loader.d.ts +10 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.js +26 -0
- package/dist/pipeline/skill-loader.js.map +1 -0
- package/dist/pipeline/skill-loader.test.d.ts +2 -0
- package/dist/pipeline/skill-loader.test.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.test.js +38 -0
- package/dist/pipeline/skill-loader.test.js.map +1 -0
- package/dist/pipeline/stage-executor.d.ts +26 -0
- package/dist/pipeline/stage-executor.d.ts.map +1 -0
- package/dist/pipeline/stage-executor.js +540 -0
- package/dist/pipeline/stage-executor.js.map +1 -0
- package/dist/pipeline/stage-resolver.d.ts +3 -0
- package/dist/pipeline/stage-resolver.d.ts.map +1 -0
- package/dist/pipeline/stage-resolver.js +6 -0
- package/dist/pipeline/stage-resolver.js.map +1 -0
- package/dist/pipeline/startup-executor.d.ts +3 -0
- package/dist/pipeline/startup-executor.d.ts.map +1 -0
- package/dist/pipeline/startup-executor.js +23 -0
- package/dist/pipeline/startup-executor.js.map +1 -0
- package/dist/pipeline/startup-executor.test.d.ts +2 -0
- package/dist/pipeline/startup-executor.test.d.ts.map +1 -0
- package/dist/pipeline/startup-executor.test.js +34 -0
- package/dist/pipeline/startup-executor.test.js.map +1 -0
- package/dist/pipeline/types.d.ts +28 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +12 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/repo-resolver.d.ts +10 -0
- package/dist/repo-resolver.d.ts.map +1 -0
- package/dist/repo-resolver.js +39 -0
- package/dist/repo-resolver.js.map +1 -0
- package/dist/spawners/direct-engine-spawner.d.ts +8 -0
- package/dist/spawners/direct-engine-spawner.d.ts.map +1 -0
- package/dist/spawners/direct-engine-spawner.js +24 -0
- package/dist/spawners/direct-engine-spawner.js.map +1 -0
- package/dist/state/run-store.d.ts +74 -0
- package/dist/state/run-store.d.ts.map +1 -0
- package/dist/state/run-store.js +269 -0
- package/dist/state/run-store.js.map +1 -0
- package/dist/state/run-store.test.d.ts +2 -0
- package/dist/state/run-store.test.d.ts.map +1 -0
- package/dist/state/run-store.test.js +130 -0
- package/dist/state/run-store.test.js.map +1 -0
- package/dist/state/state-machine.d.ts +6 -0
- package/dist/state/state-machine.d.ts.map +1 -0
- package/dist/state/state-machine.js +27 -0
- package/dist/state/state-machine.js.map +1 -0
- package/dist/state/status-derivation.d.ts +12 -0
- package/dist/state/status-derivation.d.ts.map +1 -0
- package/dist/state/status-derivation.js +30 -0
- package/dist/state/status-derivation.js.map +1 -0
- package/package.json +7 -4
- package/ARCHITECTURE.md +0 -57
- package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +0 -4
- package/src/__tests__/engine.conditions-group.test.ts +0 -244
- package/src/__tests__/engine.conditions.test.ts +0 -186
- package/src/__tests__/engine.resume.test.ts +0 -108
- package/src/__tests__/engine.script-stage.test.ts +0 -125
- package/src/engine.context-event.test.ts +0 -175
- package/src/engine.ts +0 -491
- package/src/events.ts +0 -167
- package/src/pipeline/agent-loader.test.ts +0 -151
- package/src/pipeline/agent-loader.ts +0 -39
- package/src/pipeline/condition-evaluator.test.ts +0 -129
- package/src/pipeline/condition-evaluator.ts +0 -121
- package/src/pipeline/context-pack-loader.ts +0 -63
- package/src/pipeline/context-propagation.test.ts +0 -237
- package/src/pipeline/context-propagation.ts +0 -235
- package/src/pipeline/contract-loader.ts +0 -41
- package/src/pipeline/group-orchestrator.ts +0 -483
- package/src/pipeline/hook-executor.test.ts +0 -121
- package/src/pipeline/hook-executor.ts +0 -87
- package/src/pipeline/invariants-loader.test.ts +0 -51
- package/src/pipeline/invariants-loader.ts +0 -14
- package/src/pipeline/loader.test.ts +0 -128
- package/src/pipeline/loader.ts +0 -149
- package/src/pipeline/output-validator.test.ts +0 -100
- package/src/pipeline/output-validator.ts +0 -40
- package/src/pipeline/post-validator.ts +0 -124
- package/src/pipeline/skill-loader.test.ts +0 -44
- package/src/pipeline/skill-loader.ts +0 -32
- package/src/pipeline/stage-executor.ts +0 -654
- package/src/pipeline/stage-resolver.ts +0 -8
- package/src/pipeline/startup-executor.test.ts +0 -37
- package/src/pipeline/startup-executor.ts +0 -32
- package/src/pipeline/types.ts +0 -42
- package/src/repo-resolver.ts +0 -61
- package/src/spawners/direct-engine-spawner.ts +0 -26
- package/src/state/run-store.test.ts +0 -157
- package/src/state/run-store.ts +0 -362
- package/src/state/state-machine.ts +0 -35
- package/src/state/status-derivation.ts +0 -36
- package/tests/context-pack-loader.test.ts +0 -113
- package/tests/context-propagation.test.ts +0 -267
- package/tests/direct-engine-spawner.test.ts +0 -102
- package/tests/e2e/feature-v5.test.ts +0 -57
- package/tests/events.test.ts +0 -56
- package/tests/fixtures/agents/test-agent.agent.yaml +0 -5
- package/tests/fixtures/contracts/code-gen.contract.yaml +0 -6
- package/tests/fixtures/contracts/qa-gate.contract.yaml +0 -15
- package/tests/fixtures/contracts/test-contract.contract.yaml +0 -6
- package/tests/fixtures/pipelines/group-test.pipeline.yaml +0 -40
- package/tests/fixtures/pipelines/simple.pipeline.yaml +0 -15
- package/tests/fixtures/pipelines/two-stage.pipeline.yaml +0 -24
- package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +0 -75
- package/tests/fixtures/test-project/agents/test-agent.agent.yaml +0 -5
- package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +0 -6
- package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +0 -6
- package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +0 -8
- package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +0 -15
- package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +0 -7
- package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +0 -6
- package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +0 -26
- package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +0 -40
- package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +0 -19
- package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +0 -19
- package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +0 -15
- package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +0 -38
- package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +0 -38
- package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +0 -15
- package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +0 -24
- package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +0 -19
- package/tests/loader.test.ts +0 -385
- package/tests/post-validator.test.ts +0 -297
- package/tests/repo-resolver.test.ts +0 -102
- package/tests/run-store.test.ts +0 -143
- package/tests/state-machine.test.ts +0 -110
- package/tests/unit/context-propagation.test.ts +0 -45
- package/tests/unit/engine.test.ts +0 -770
- package/tests/unit/group-loop.test.ts +0 -389
- package/tests/unit/group-parallel.test.ts +0 -423
- package/tests/unit/state/status-derivation.test.ts +0 -88
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -14
package/src/state/run-store.ts
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
// Persistence layer for pipeline runs
|
|
2
|
-
// Three implementations: InMemoryRunStore (tests), SQLiteRunStore (local), PgRunStore (postgres)
|
|
3
|
-
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
5
|
-
import type { PipelineRun } from '@studio-foundation/contracts';
|
|
6
|
-
|
|
7
|
-
export interface RunStore {
|
|
8
|
-
savePipelineRun(run: PipelineRun): void;
|
|
9
|
-
getPipelineRun(id: string): PipelineRun | null;
|
|
10
|
-
listPipelineRuns(options?: { limit?: number; status?: string }): PipelineRun[];
|
|
11
|
-
getLatestRun(pipelineName?: string): PipelineRun | null;
|
|
12
|
-
saveLogPath(runId: string, logPath: string): void;
|
|
13
|
-
getLogPath(runId: string): string | null;
|
|
14
|
-
close?(): void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface AsyncRunStore {
|
|
18
|
-
savePipelineRun(run: PipelineRun): Promise<void>;
|
|
19
|
-
getPipelineRun(id: string): Promise<PipelineRun | null>;
|
|
20
|
-
listPipelineRuns(options?: { limit?: number; status?: string }): Promise<PipelineRun[]>;
|
|
21
|
-
getLatestRun(pipelineName?: string): Promise<PipelineRun | null>;
|
|
22
|
-
saveLogPath(runId: string, logPath: string): Promise<void>;
|
|
23
|
-
getLogPath(runId: string): Promise<string | null>;
|
|
24
|
-
close?(): Promise<void>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type AnyRunStore = RunStore | AsyncRunStore;
|
|
28
|
-
|
|
29
|
-
// In-memory store for tests and simple usage
|
|
30
|
-
export class InMemoryRunStore implements RunStore {
|
|
31
|
-
private runs: Map<string, PipelineRun> = new Map();
|
|
32
|
-
private logPaths: Map<string, string> = new Map();
|
|
33
|
-
|
|
34
|
-
savePipelineRun(run: PipelineRun): void {
|
|
35
|
-
this.runs.set(run.id, structuredClone(run));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
getPipelineRun(id: string): PipelineRun | null {
|
|
39
|
-
const run = this.runs.get(id);
|
|
40
|
-
return run ? structuredClone(run) : null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
listPipelineRuns(options?: { limit?: number; status?: string }): PipelineRun[] {
|
|
44
|
-
let runs = Array.from(this.runs.values());
|
|
45
|
-
|
|
46
|
-
if (options?.status) {
|
|
47
|
-
runs = runs.filter(r => r.status === options.status);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Sort by started_at descending
|
|
51
|
-
runs.sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
52
|
-
|
|
53
|
-
if (options?.limit) {
|
|
54
|
-
runs = runs.slice(0, options.limit);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return runs.map(r => structuredClone(r));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getLatestRun(pipelineName?: string): PipelineRun | null {
|
|
61
|
-
let runs = Array.from(this.runs.values());
|
|
62
|
-
|
|
63
|
-
if (pipelineName) {
|
|
64
|
-
runs = runs.filter(r => r.pipeline_name === pipelineName);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (runs.length === 0) return null;
|
|
68
|
-
|
|
69
|
-
runs.sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
70
|
-
return structuredClone(runs[0]);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
saveLogPath(runId: string, logPath: string): void {
|
|
74
|
-
this.logPaths.set(runId, logPath);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
getLogPath(runId: string): string | null {
|
|
78
|
-
return this.logPaths.get(runId) ?? null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// SQLite store for production persistence
|
|
83
|
-
// Uses better-sqlite3 (synchronous, simple, no migrations)
|
|
84
|
-
// Stores the entire PipelineRun as JSON in a single column — simple, queryable by status
|
|
85
|
-
export class SQLiteRunStore implements RunStore {
|
|
86
|
-
private db: import('better-sqlite3').Database;
|
|
87
|
-
|
|
88
|
-
constructor(dbPath: string) {
|
|
89
|
-
const _require = createRequire(import.meta.url);
|
|
90
|
-
const Database = _require('better-sqlite3') as new (path: string) => import('better-sqlite3').Database;
|
|
91
|
-
this.db = new Database(dbPath);
|
|
92
|
-
this.db.pragma('journal_mode = WAL');
|
|
93
|
-
this.initSchema();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private initSchema(): void {
|
|
97
|
-
this.db.exec(`
|
|
98
|
-
CREATE TABLE IF NOT EXISTS pipeline_runs (
|
|
99
|
-
id TEXT PRIMARY KEY,
|
|
100
|
-
pipeline_name TEXT NOT NULL,
|
|
101
|
-
status TEXT NOT NULL,
|
|
102
|
-
result TEXT NOT NULL,
|
|
103
|
-
started_at TEXT NOT NULL,
|
|
104
|
-
completed_at TEXT,
|
|
105
|
-
log_path TEXT,
|
|
106
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
CREATE INDEX IF NOT EXISTS idx_pipeline_runs_status
|
|
110
|
-
ON pipeline_runs(status);
|
|
111
|
-
CREATE INDEX IF NOT EXISTS idx_pipeline_runs_created
|
|
112
|
-
ON pipeline_runs(created_at DESC);
|
|
113
|
-
`);
|
|
114
|
-
|
|
115
|
-
// Migration: add log_path column to existing databases
|
|
116
|
-
try {
|
|
117
|
-
this.db.exec('ALTER TABLE pipeline_runs ADD COLUMN log_path TEXT');
|
|
118
|
-
} catch {
|
|
119
|
-
// Column already exists — ignore
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Migration: add parent_run_id column to existing databases
|
|
123
|
-
try {
|
|
124
|
-
this.db.exec('ALTER TABLE pipeline_runs ADD COLUMN parent_run_id TEXT');
|
|
125
|
-
this.db.exec('CREATE INDEX IF NOT EXISTS idx_pipeline_runs_parent ON pipeline_runs(parent_run_id)');
|
|
126
|
-
} catch {
|
|
127
|
-
// Column already exists — ignore
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
savePipelineRun(run: PipelineRun): void {
|
|
132
|
-
this.db.prepare(`
|
|
133
|
-
INSERT INTO pipeline_runs (id, pipeline_name, status, result, started_at, completed_at, parent_run_id)
|
|
134
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
135
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
136
|
-
pipeline_name = excluded.pipeline_name,
|
|
137
|
-
status = excluded.status,
|
|
138
|
-
result = excluded.result,
|
|
139
|
-
started_at = excluded.started_at,
|
|
140
|
-
completed_at = excluded.completed_at,
|
|
141
|
-
parent_run_id = excluded.parent_run_id
|
|
142
|
-
`).run(
|
|
143
|
-
run.id,
|
|
144
|
-
run.pipeline_name,
|
|
145
|
-
run.status,
|
|
146
|
-
JSON.stringify(run),
|
|
147
|
-
run.started_at,
|
|
148
|
-
run.completed_at ?? null,
|
|
149
|
-
run.parent_run_id ?? null,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
getPipelineRun(id: string): PipelineRun | null {
|
|
154
|
-
const row = this.db.prepare('SELECT result FROM pipeline_runs WHERE id = ?').get(id) as
|
|
155
|
-
| { result: string }
|
|
156
|
-
| undefined;
|
|
157
|
-
|
|
158
|
-
if (!row) return null;
|
|
159
|
-
return JSON.parse(row.result) as PipelineRun;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
listPipelineRuns(options?: { limit?: number; status?: string }): PipelineRun[] {
|
|
163
|
-
let sql = 'SELECT result FROM pipeline_runs';
|
|
164
|
-
const params: unknown[] = [];
|
|
165
|
-
|
|
166
|
-
if (options?.status) {
|
|
167
|
-
sql += ' WHERE status = ?';
|
|
168
|
-
params.push(options.status);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
sql += ' ORDER BY created_at DESC';
|
|
172
|
-
|
|
173
|
-
if (options?.limit) {
|
|
174
|
-
sql += ' LIMIT ?';
|
|
175
|
-
params.push(options.limit);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const rows = this.db.prepare(sql).all(...params) as Array<{ result: string }>;
|
|
179
|
-
return rows.map(row => JSON.parse(row.result) as PipelineRun);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
getLatestRun(pipelineName?: string): PipelineRun | null {
|
|
183
|
-
let sql = 'SELECT result FROM pipeline_runs';
|
|
184
|
-
const params: unknown[] = [];
|
|
185
|
-
|
|
186
|
-
if (pipelineName) {
|
|
187
|
-
sql += ' WHERE pipeline_name = ?';
|
|
188
|
-
params.push(pipelineName);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
sql += ' ORDER BY created_at DESC LIMIT 1';
|
|
192
|
-
|
|
193
|
-
const row = this.db.prepare(sql).get(...params) as { result: string } | undefined;
|
|
194
|
-
if (!row) return null;
|
|
195
|
-
return JSON.parse(row.result) as PipelineRun;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
saveLogPath(runId: string, logPath: string): void {
|
|
199
|
-
this.db.prepare('UPDATE pipeline_runs SET log_path = ? WHERE id = ?').run(logPath, runId);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
getLogPath(runId: string): string | null {
|
|
203
|
-
const row = this.db.prepare('SELECT log_path FROM pipeline_runs WHERE id = ?').get(runId) as
|
|
204
|
-
| { log_path: string | null }
|
|
205
|
-
| undefined;
|
|
206
|
-
return row?.log_path ?? null;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
close(): void {
|
|
210
|
-
this.db.close();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// PostgreSQL store — async implementation using raw pg (no Prisma)
|
|
215
|
-
// Creates its own `studio_pipeline_runs` table on first use (like SQLiteRunStore does with initSchema).
|
|
216
|
-
// Stores the entire PipelineRun as JSON in `result` — same pattern as SQLiteRunStore.
|
|
217
|
-
// Uses table prefix `studio_` to avoid conflicts with the user app's own tables.
|
|
218
|
-
export class PgRunStore implements AsyncRunStore {
|
|
219
|
-
private pool: import('pg').Pool;
|
|
220
|
-
private schemaReady: Promise<void> | null = null;
|
|
221
|
-
|
|
222
|
-
constructor(connectionString: string) {
|
|
223
|
-
const _require = createRequire(import.meta.url);
|
|
224
|
-
const { Pool } = _require('pg') as typeof import('pg');
|
|
225
|
-
this.pool = new Pool({ connectionString });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private ensureSchema(): Promise<void> {
|
|
229
|
-
if (!this.schemaReady) {
|
|
230
|
-
this.schemaReady = this.initSchema();
|
|
231
|
-
}
|
|
232
|
-
return this.schemaReady;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private async initSchema(): Promise<void> {
|
|
236
|
-
await this.pool.query(`
|
|
237
|
-
CREATE TABLE IF NOT EXISTS studio_pipeline_runs (
|
|
238
|
-
id TEXT PRIMARY KEY,
|
|
239
|
-
pipeline_name TEXT NOT NULL,
|
|
240
|
-
status TEXT NOT NULL,
|
|
241
|
-
result TEXT NOT NULL,
|
|
242
|
-
started_at TEXT NOT NULL,
|
|
243
|
-
completed_at TEXT,
|
|
244
|
-
log_path TEXT,
|
|
245
|
-
parent_run_id TEXT,
|
|
246
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
247
|
-
)
|
|
248
|
-
`);
|
|
249
|
-
await this.pool.query(`
|
|
250
|
-
CREATE INDEX IF NOT EXISTS idx_studio_pipeline_runs_status
|
|
251
|
-
ON studio_pipeline_runs(status)
|
|
252
|
-
`);
|
|
253
|
-
await this.pool.query(`
|
|
254
|
-
CREATE INDEX IF NOT EXISTS idx_studio_pipeline_runs_created
|
|
255
|
-
ON studio_pipeline_runs(created_at DESC)
|
|
256
|
-
`);
|
|
257
|
-
await this.pool.query(`
|
|
258
|
-
CREATE INDEX IF NOT EXISTS idx_studio_pipeline_runs_parent
|
|
259
|
-
ON studio_pipeline_runs(parent_run_id)
|
|
260
|
-
`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async savePipelineRun(run: PipelineRun): Promise<void> {
|
|
264
|
-
await this.ensureSchema();
|
|
265
|
-
await this.pool.query(
|
|
266
|
-
`INSERT INTO studio_pipeline_runs
|
|
267
|
-
(id, pipeline_name, status, result, started_at, completed_at, parent_run_id)
|
|
268
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
269
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
270
|
-
pipeline_name = EXCLUDED.pipeline_name,
|
|
271
|
-
status = EXCLUDED.status,
|
|
272
|
-
result = EXCLUDED.result,
|
|
273
|
-
started_at = EXCLUDED.started_at,
|
|
274
|
-
completed_at = EXCLUDED.completed_at,
|
|
275
|
-
parent_run_id = EXCLUDED.parent_run_id`,
|
|
276
|
-
[
|
|
277
|
-
run.id,
|
|
278
|
-
run.pipeline_name,
|
|
279
|
-
run.status,
|
|
280
|
-
JSON.stringify(run),
|
|
281
|
-
run.started_at,
|
|
282
|
-
run.completed_at ?? null,
|
|
283
|
-
run.parent_run_id ?? null,
|
|
284
|
-
]
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async getPipelineRun(id: string): Promise<PipelineRun | null> {
|
|
289
|
-
await this.ensureSchema();
|
|
290
|
-
const res = await this.pool.query<{ result: string }>(
|
|
291
|
-
'SELECT result FROM studio_pipeline_runs WHERE id = $1',
|
|
292
|
-
[id]
|
|
293
|
-
);
|
|
294
|
-
if (res.rows.length === 0) return null;
|
|
295
|
-
return JSON.parse(res.rows[0].result) as PipelineRun;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async listPipelineRuns(options?: { limit?: number; status?: string }): Promise<PipelineRun[]> {
|
|
299
|
-
await this.ensureSchema();
|
|
300
|
-
const params: unknown[] = [];
|
|
301
|
-
let sql = 'SELECT result FROM studio_pipeline_runs';
|
|
302
|
-
|
|
303
|
-
if (options?.status) {
|
|
304
|
-
params.push(options.status);
|
|
305
|
-
sql += ` WHERE status = $${params.length}`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
sql += ' ORDER BY created_at DESC';
|
|
309
|
-
|
|
310
|
-
if (options?.limit) {
|
|
311
|
-
params.push(options.limit);
|
|
312
|
-
sql += ` LIMIT $${params.length}`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const res = await this.pool.query<{ result: string }>(sql, params);
|
|
316
|
-
return res.rows.map((r) => JSON.parse(r.result) as PipelineRun);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async getLatestRun(pipelineName?: string): Promise<PipelineRun | null> {
|
|
320
|
-
await this.ensureSchema();
|
|
321
|
-
const params: unknown[] = [];
|
|
322
|
-
let sql = 'SELECT result FROM studio_pipeline_runs';
|
|
323
|
-
|
|
324
|
-
if (pipelineName) {
|
|
325
|
-
params.push(pipelineName);
|
|
326
|
-
sql += ` WHERE pipeline_name = $${params.length}`;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
sql += ' ORDER BY created_at DESC LIMIT 1';
|
|
330
|
-
|
|
331
|
-
const res = await this.pool.query<{ result: string }>(sql, params);
|
|
332
|
-
if (res.rows.length === 0) return null;
|
|
333
|
-
return JSON.parse(res.rows[0].result) as PipelineRun;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async saveLogPath(runId: string, logPath: string): Promise<void> {
|
|
337
|
-
await this.ensureSchema();
|
|
338
|
-
await this.pool.query(
|
|
339
|
-
'UPDATE studio_pipeline_runs SET log_path = $1 WHERE id = $2',
|
|
340
|
-
[logPath, runId]
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async getLogPath(runId: string): Promise<string | null> {
|
|
345
|
-
await this.ensureSchema();
|
|
346
|
-
const res = await this.pool.query<{ log_path: string | null }>(
|
|
347
|
-
'SELECT log_path FROM studio_pipeline_runs WHERE id = $1',
|
|
348
|
-
[runId]
|
|
349
|
-
);
|
|
350
|
-
return res.rows[0]?.log_path ?? null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async close(): Promise<void> {
|
|
354
|
-
await this.pool.end();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Test-only: wipe table contents. Never call in production. */
|
|
358
|
-
async dangerouslyTruncateForTests(): Promise<void> {
|
|
359
|
-
await this.ensureSchema();
|
|
360
|
-
await this.pool.query('TRUNCATE TABLE studio_pipeline_runs');
|
|
361
|
-
}
|
|
362
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Stage lifecycle state machine
|
|
2
|
-
// Keeps it simple: linear transitions, no DAG
|
|
3
|
-
|
|
4
|
-
export type StageLifecycleState = 'pending' | 'running' | 'success' | 'failed' | 'skipped' | 'rejected' | 'cancelled';
|
|
5
|
-
|
|
6
|
-
type StageEvent = 'start' | 'succeed' | 'fail' | 'skip' | 'reject' | 'cancel';
|
|
7
|
-
|
|
8
|
-
const VALID_TRANSITIONS: Record<string, StageLifecycleState> = {
|
|
9
|
-
'pending:start': 'running',
|
|
10
|
-
'running:succeed': 'success',
|
|
11
|
-
'running:fail': 'failed',
|
|
12
|
-
'pending:skip': 'skipped',
|
|
13
|
-
'running:reject': 'rejected',
|
|
14
|
-
'running:cancel': 'cancelled',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function isValidTransition(from: StageLifecycleState, to: StageLifecycleState): boolean {
|
|
18
|
-
for (const [key, target] of Object.entries(VALID_TRANSITIONS)) {
|
|
19
|
-
if (key.startsWith(`${from}:`) && target === to) {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function transition(current: StageLifecycleState, event: StageEvent): StageLifecycleState {
|
|
27
|
-
const key = `${current}:${event}`;
|
|
28
|
-
const next = VALID_TRANSITIONS[key];
|
|
29
|
-
|
|
30
|
-
if (!next) {
|
|
31
|
-
throw new Error(`Invalid state transition: ${current} + ${event}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return next;
|
|
35
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// Derive stage status from ralph result
|
|
2
|
-
// THIS IS THE CRITICAL FUNCTION
|
|
3
|
-
//
|
|
4
|
-
// In v7, each stage = 1 ralph call = 1 task.
|
|
5
|
-
// This function maps ralph's result directly to stage status.
|
|
6
|
-
//
|
|
7
|
-
// This was the #1 bug in v6: stage status didn't match task status.
|
|
8
|
-
// v7 fix: SIMPLE, DETERMINISTIC, NO MAGIC.
|
|
9
|
-
|
|
10
|
-
import type { RalphResult } from '@studio-foundation/ralph';
|
|
11
|
-
import type { StageStatus } from '@studio-foundation/contracts';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Derives stage status from ralph result.
|
|
15
|
-
*
|
|
16
|
-
* Rules (simple and exhaustive):
|
|
17
|
-
* - ralph 'success' → stage 'success'
|
|
18
|
-
* - ralph 'exhausted' → stage 'failed'
|
|
19
|
-
* - anything else → error (should never happen)
|
|
20
|
-
*/
|
|
21
|
-
export function deriveStageStatus(ralphResult: RalphResult<unknown>): StageStatus {
|
|
22
|
-
if (ralphResult.status === 'success') {
|
|
23
|
-
return 'success';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (ralphResult.status === 'exhausted') {
|
|
27
|
-
return 'failed';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (ralphResult.status === 'cancelled') {
|
|
31
|
-
return 'cancelled';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Should never reach here if RalphResult types are exhaustive
|
|
35
|
-
throw new Error(`Unknown ralph status: ${JSON.stringify(ralphResult)}`);
|
|
36
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'fs/promises';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
import { loadContextPacks } from '../src/pipeline/context-pack-loader.js';
|
|
6
|
-
|
|
7
|
-
describe('loadContextPacks', () => {
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
let workspaceDir: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'studio-packs-'));
|
|
13
|
-
workspaceDir = path.join(tmpDir, 'workspace');
|
|
14
|
-
await fs.mkdir(path.join(tmpDir, 'context-packs'), { recursive: true });
|
|
15
|
-
await fs.mkdir(workspaceDir, { recursive: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(async () => {
|
|
19
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('loads a pack with only inline sections', async () => {
|
|
23
|
-
await fs.writeFile(
|
|
24
|
-
path.join(tmpDir, 'context-packs', 'coding-standards.yaml'),
|
|
25
|
-
`name: Coding Standards\ndescription: Our standards\nversion: 1\ninline:\n - title: "Naming"\n content: "Use camelCase"\n - title: "Errors"\n content: "Always catch"`
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
const result = await loadContextPacks(['coding-standards'], tmpDir);
|
|
29
|
-
|
|
30
|
-
expect(result).toHaveLength(1);
|
|
31
|
-
expect(result[0].name).toBe('Coding Standards');
|
|
32
|
-
expect(result[0].description).toBe('Our standards');
|
|
33
|
-
expect(result[0].sections).toHaveLength(2);
|
|
34
|
-
expect(result[0].sections[0]).toEqual({ title: 'Naming', content: 'Use camelCase' });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('loads a pack with file sections read from workspace', async () => {
|
|
38
|
-
await fs.writeFile(path.join(workspaceDir, 'STYLE.md'), '# Style Guide\nUse tabs.');
|
|
39
|
-
await fs.writeFile(
|
|
40
|
-
path.join(tmpDir, 'context-packs', 'style.yaml'),
|
|
41
|
-
`name: Style Guide\nversion: 1\nfiles:\n - path: STYLE.md`
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const result = await loadContextPacks(['style'], tmpDir, workspaceDir);
|
|
45
|
-
|
|
46
|
-
expect(result[0].sections).toHaveLength(1);
|
|
47
|
-
expect(result[0].sections[0].title).toBe('STYLE.md');
|
|
48
|
-
expect(result[0].sections[0].content).toContain('# Style Guide');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('puts file sections before inline sections', async () => {
|
|
52
|
-
await fs.writeFile(path.join(workspaceDir, 'README.md'), 'Read me.');
|
|
53
|
-
await fs.writeFile(
|
|
54
|
-
path.join(tmpDir, 'context-packs', 'mixed.yaml'),
|
|
55
|
-
`name: Mixed\nversion: 1\nfiles:\n - path: README.md\ninline:\n - title: "Rule"\n content: "Follow rules"`
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const result = await loadContextPacks(['mixed'], tmpDir, workspaceDir);
|
|
59
|
-
|
|
60
|
-
expect(result[0].sections[0].title).toBe('README.md');
|
|
61
|
-
expect(result[0].sections[1].title).toBe('Rule');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('throws a clear error when pack file does not exist', async () => {
|
|
65
|
-
await expect(
|
|
66
|
-
loadContextPacks(['nonexistent'], tmpDir)
|
|
67
|
-
).rejects.toThrow(/context pack.*nonexistent.*not found/i);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('throws a clear error when referenced workspace file does not exist', async () => {
|
|
71
|
-
await fs.writeFile(
|
|
72
|
-
path.join(tmpDir, 'context-packs', 'bad-pack.yaml'),
|
|
73
|
-
`name: Bad Pack\nversion: 1\nfiles:\n - path: missing-file.md`
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
await expect(
|
|
77
|
-
loadContextPacks(['bad-pack'], tmpDir, workspaceDir)
|
|
78
|
-
).rejects.toThrow(/file.*missing-file\.md.*not found/i);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('throws when files[] referenced but workspacePath not provided', async () => {
|
|
82
|
-
await fs.writeFile(
|
|
83
|
-
path.join(tmpDir, 'context-packs', 'needs-ws.yaml'),
|
|
84
|
-
`name: Needs WS\nversion: 1\nfiles:\n - path: some.md`
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
await expect(
|
|
88
|
-
loadContextPacks(['needs-ws'], tmpDir, undefined)
|
|
89
|
-
).rejects.toThrow(/workspace.*not configured/i);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('loads multiple packs preserving order', async () => {
|
|
93
|
-
await fs.writeFile(
|
|
94
|
-
path.join(tmpDir, 'context-packs', 'pack-a.yaml'),
|
|
95
|
-
`name: Pack A\nversion: 1\ninline:\n - title: A\n content: a`
|
|
96
|
-
);
|
|
97
|
-
await fs.writeFile(
|
|
98
|
-
path.join(tmpDir, 'context-packs', 'pack-b.yaml'),
|
|
99
|
-
`name: Pack B\nversion: 1\ninline:\n - title: B\n content: b`
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const result = await loadContextPacks(['pack-a', 'pack-b'], tmpDir);
|
|
103
|
-
|
|
104
|
-
expect(result).toHaveLength(2);
|
|
105
|
-
expect(result[0].name).toBe('Pack A');
|
|
106
|
-
expect(result[1].name).toBe('Pack B');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('returns empty array when packNames is empty', async () => {
|
|
110
|
-
const result = await loadContextPacks([], tmpDir);
|
|
111
|
-
expect(result).toEqual([]);
|
|
112
|
-
});
|
|
113
|
-
});
|