substrate-ai 0.5.0 → 0.5.1
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/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-
|
|
2
|
+
import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-BD0Ugp7F.js";
|
|
3
3
|
import { createLogger } from "../logger-D2fS2ccL.js";
|
|
4
4
|
import { AdapterRegistry } from "../adapter-registry-BkUvZSKJ.js";
|
|
5
5
|
import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DtZW1maj.js";
|
|
@@ -19,7 +19,7 @@ import yaml from "js-yaml";
|
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import * as path$1 from "node:path";
|
|
21
21
|
import { isAbsolute, join as join$1 } from "node:path";
|
|
22
|
-
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
|
|
22
|
+
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
|
|
23
23
|
import { access as access$1, readFile as readFile$1 } from "node:fs/promises";
|
|
24
24
|
import { createInterface } from "node:readline";
|
|
25
25
|
import { homedir } from "os";
|
|
@@ -574,8 +574,8 @@ async function directoryExists(path$2) {
|
|
|
574
574
|
*/
|
|
575
575
|
async function runInitAction(options) {
|
|
576
576
|
const { pack: packName, projectRoot, outputFormat, force = false, yes: nonInteractive = false } = options;
|
|
577
|
-
const packPath = join(projectRoot, "packs", packName);
|
|
578
577
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
578
|
+
const packPath = join(dbRoot, "packs", packName);
|
|
579
579
|
const substrateDir = join(dbRoot, ".substrate");
|
|
580
580
|
const dbPath = join(substrateDir, "substrate.db");
|
|
581
581
|
const configPath = join(substrateDir, "config.yaml");
|
|
@@ -1007,16 +1007,30 @@ function registerConfigCommand(program, _version) {
|
|
|
1007
1007
|
//#endregion
|
|
1008
1008
|
//#region src/cli/commands/resume.ts
|
|
1009
1009
|
const logger$16 = createLogger("resume-cmd");
|
|
1010
|
+
/**
|
|
1011
|
+
* Map internal orchestrator phase names to pipeline event protocol phase names.
|
|
1012
|
+
*/
|
|
1013
|
+
function mapInternalPhaseToEventPhase(internalPhase) {
|
|
1014
|
+
switch (internalPhase) {
|
|
1015
|
+
case "IN_STORY_CREATION": return "create-story";
|
|
1016
|
+
case "IN_DEV": return "dev-story";
|
|
1017
|
+
case "IN_REVIEW": return "code-review";
|
|
1018
|
+
case "IN_MINOR_FIX":
|
|
1019
|
+
case "IN_MAJOR_FIX": return "fix";
|
|
1020
|
+
case "IN_TEST_PLANNING": return "test-planning";
|
|
1021
|
+
default: return null;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1010
1024
|
async function runResumeAction(options) {
|
|
1011
|
-
const { runId: specifiedRunId, stopAfter, outputFormat, projectRoot, concurrency, pack: packName, registry } = options;
|
|
1025
|
+
const { runId: specifiedRunId, stopAfter, outputFormat, projectRoot, concurrency, pack: packName, events: eventsFlag, registry } = options;
|
|
1012
1026
|
if (stopAfter !== void 0 && !VALID_PHASES.includes(stopAfter)) {
|
|
1013
1027
|
const errorMsg = `Invalid phase: "${stopAfter}". Valid phases: ${VALID_PHASES.join(", ")}`;
|
|
1014
1028
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
1015
1029
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
1016
1030
|
return 1;
|
|
1017
1031
|
}
|
|
1018
|
-
const packPath = join(projectRoot, "packs", packName);
|
|
1019
1032
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
1033
|
+
const packPath = join(dbRoot, "packs", packName);
|
|
1020
1034
|
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
1021
1035
|
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
1022
1036
|
if (!existsSync(dbPath) && !existsSync(doltDir)) {
|
|
@@ -1086,6 +1100,7 @@ async function runResumeAction(options) {
|
|
|
1086
1100
|
concept,
|
|
1087
1101
|
concurrency,
|
|
1088
1102
|
outputFormat,
|
|
1103
|
+
events: eventsFlag,
|
|
1089
1104
|
existingRunId: runId,
|
|
1090
1105
|
projectRoot,
|
|
1091
1106
|
registry
|
|
@@ -1103,7 +1118,7 @@ async function runResumeAction(options) {
|
|
|
1103
1118
|
}
|
|
1104
1119
|
}
|
|
1105
1120
|
async function runFullPipelineFromPhase(options) {
|
|
1106
|
-
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, existingRunId, projectRoot, registry: injectedRegistry } = options;
|
|
1121
|
+
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, events: eventsFlag, existingRunId, projectRoot, registry: injectedRegistry } = options;
|
|
1107
1122
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
1108
1123
|
const adapter = createDatabaseAdapter({
|
|
1109
1124
|
backend: "auto",
|
|
@@ -1143,6 +1158,8 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1143
1158
|
let runId;
|
|
1144
1159
|
if (existingRunId !== void 0) runId = existingRunId;
|
|
1145
1160
|
else runId = await phaseOrchestrator.startRun(concept, startPhase);
|
|
1161
|
+
let ndjsonEmitter;
|
|
1162
|
+
if (eventsFlag === true) ndjsonEmitter = createEventEmitter(process.stdout);
|
|
1146
1163
|
const phaseOrder = [
|
|
1147
1164
|
"analysis",
|
|
1148
1165
|
"planning",
|
|
@@ -1239,12 +1256,87 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1239
1256
|
config: {
|
|
1240
1257
|
maxConcurrency: concurrency,
|
|
1241
1258
|
maxReviewCycles: 2,
|
|
1242
|
-
pipelineRunId: runId
|
|
1259
|
+
pipelineRunId: runId,
|
|
1260
|
+
enableHeartbeat: eventsFlag === true
|
|
1243
1261
|
},
|
|
1244
1262
|
projectRoot,
|
|
1245
1263
|
...ingestionServer !== void 0 ? { ingestionServer } : {},
|
|
1246
1264
|
...telemetryPersistence !== void 0 ? { telemetryPersistence } : {}
|
|
1247
1265
|
});
|
|
1266
|
+
if (ndjsonEmitter !== void 0) {
|
|
1267
|
+
const resolvedKeys = await resolveStoryKeys(adapter, projectRoot, { pipelineRunId: runId });
|
|
1268
|
+
ndjsonEmitter.emit({
|
|
1269
|
+
type: "pipeline:start",
|
|
1270
|
+
ts: new Date().toISOString(),
|
|
1271
|
+
run_id: runId,
|
|
1272
|
+
stories: resolvedKeys,
|
|
1273
|
+
concurrency
|
|
1274
|
+
});
|
|
1275
|
+
eventBus.on("orchestrator:story-phase-start", (payload) => {
|
|
1276
|
+
const phase = mapInternalPhaseToEventPhase(payload.phase);
|
|
1277
|
+
if (phase !== null) ndjsonEmitter.emit({
|
|
1278
|
+
type: "story:phase",
|
|
1279
|
+
ts: new Date().toISOString(),
|
|
1280
|
+
key: payload.storyKey,
|
|
1281
|
+
phase,
|
|
1282
|
+
status: "in_progress"
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
eventBus.on("orchestrator:story-phase-complete", (payload) => {
|
|
1286
|
+
const phase = mapInternalPhaseToEventPhase(payload.phase);
|
|
1287
|
+
if (phase !== null) {
|
|
1288
|
+
const result = payload.result;
|
|
1289
|
+
ndjsonEmitter.emit({
|
|
1290
|
+
type: "story:phase",
|
|
1291
|
+
ts: new Date().toISOString(),
|
|
1292
|
+
key: payload.storyKey,
|
|
1293
|
+
phase,
|
|
1294
|
+
status: "complete",
|
|
1295
|
+
...phase === "code-review" && result?.verdict !== void 0 ? { verdict: result.verdict } : {},
|
|
1296
|
+
...phase === "create-story" && result?.story_file !== void 0 ? { file: result.story_file } : {}
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
eventBus.on("orchestrator:story-complete", (payload) => {
|
|
1301
|
+
ndjsonEmitter.emit({
|
|
1302
|
+
type: "story:done",
|
|
1303
|
+
ts: new Date().toISOString(),
|
|
1304
|
+
key: payload.storyKey,
|
|
1305
|
+
result: "success",
|
|
1306
|
+
review_cycles: payload.reviewCycles
|
|
1307
|
+
});
|
|
1308
|
+
});
|
|
1309
|
+
eventBus.on("orchestrator:story-escalated", (payload) => {
|
|
1310
|
+
const rawIssues = Array.isArray(payload.issues) ? payload.issues : [];
|
|
1311
|
+
const issues = rawIssues.map((issue) => {
|
|
1312
|
+
const iss = issue;
|
|
1313
|
+
return {
|
|
1314
|
+
severity: iss.severity ?? "unknown",
|
|
1315
|
+
file: iss.file ?? "",
|
|
1316
|
+
desc: iss.desc ?? iss.description ?? ""
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
ndjsonEmitter.emit({
|
|
1320
|
+
type: "story:escalation",
|
|
1321
|
+
ts: new Date().toISOString(),
|
|
1322
|
+
key: payload.storyKey,
|
|
1323
|
+
reason: payload.lastVerdict ?? "escalated",
|
|
1324
|
+
cycles: payload.reviewCycles ?? 0,
|
|
1325
|
+
issues,
|
|
1326
|
+
...payload.diagnosis !== void 0 ? { diagnosis: payload.diagnosis } : {}
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
eventBus.on("orchestrator:heartbeat", (payload) => {
|
|
1330
|
+
ndjsonEmitter.emit({
|
|
1331
|
+
type: "pipeline:heartbeat",
|
|
1332
|
+
ts: new Date().toISOString(),
|
|
1333
|
+
run_id: payload.runId,
|
|
1334
|
+
active_dispatches: payload.activeDispatches,
|
|
1335
|
+
completed_dispatches: payload.completedDispatches,
|
|
1336
|
+
queued_dispatches: payload.queuedDispatches
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1248
1340
|
eventBus.on("orchestrator:story-phase-complete", (payload) => {
|
|
1249
1341
|
try {
|
|
1250
1342
|
const result = payload.result;
|
|
@@ -1268,6 +1360,13 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1268
1360
|
const storyKeys = await resolveStoryKeys(adapter, projectRoot, { pipelineRunId: runId });
|
|
1269
1361
|
if (storyKeys.length === 0 && outputFormat === "human") process.stdout.write("[IMPLEMENTATION] No stories found for this run. Check solutioning phase output.\n");
|
|
1270
1362
|
await orchestrator.run(storyKeys);
|
|
1363
|
+
if (ndjsonEmitter !== void 0) ndjsonEmitter.emit({
|
|
1364
|
+
type: "pipeline:complete",
|
|
1365
|
+
ts: new Date().toISOString(),
|
|
1366
|
+
succeeded: storyKeys,
|
|
1367
|
+
failed: [],
|
|
1368
|
+
escalated: []
|
|
1369
|
+
});
|
|
1271
1370
|
if (outputFormat === "human") process.stdout.write("[IMPLEMENTATION] Complete\n");
|
|
1272
1371
|
}
|
|
1273
1372
|
if (stopAfter !== void 0 && currentPhase === stopAfter) {
|
|
@@ -1330,7 +1429,7 @@ async function runFullPipelineFromPhase(options) {
|
|
|
1330
1429
|
}
|
|
1331
1430
|
}
|
|
1332
1431
|
function registerResumeCommand(program, _version = "0.0.0", projectRoot = process.cwd(), registry) {
|
|
1333
|
-
program.command("resume").description("Resume a previously interrupted pipeline run").option("--run-id <id>", "Pipeline run ID to resume (defaults to latest)").option("--pack <name>", "Methodology pack name", "bmad").option("--stop-after <phase>", "Stop pipeline after this phase completes (overrides saved state)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
|
|
1432
|
+
program.command("resume").description("Resume a previously interrupted pipeline run").option("--run-id <id>", "Pipeline run ID to resume (defaults to latest)").option("--pack <name>", "Methodology pack name", "bmad").option("--stop-after <phase>", "Stop pipeline after this phase completes (overrides saved state)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--events", "Emit structured NDJSON events on stdout for programmatic consumption").action(async (opts) => {
|
|
1334
1433
|
const outputFormat = opts.outputFormat === "json" ? "json" : "human";
|
|
1335
1434
|
const exitCode = await runResumeAction({
|
|
1336
1435
|
runId: opts.runId,
|
|
@@ -1339,6 +1438,7 @@ function registerResumeCommand(program, _version = "0.0.0", projectRoot = proces
|
|
|
1339
1438
|
projectRoot: opts.projectRoot,
|
|
1340
1439
|
concurrency: opts.concurrency,
|
|
1341
1440
|
pack: opts.pack,
|
|
1441
|
+
events: opts.events,
|
|
1342
1442
|
registry
|
|
1343
1443
|
});
|
|
1344
1444
|
process.exitCode = exitCode;
|
|
@@ -2031,7 +2131,7 @@ async function runAmendAction(options) {
|
|
|
2031
2131
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
2032
2132
|
const dbDir = join(dbRoot, ".substrate");
|
|
2033
2133
|
const dbPath = join(dbDir, "substrate.db");
|
|
2034
|
-
const packPath = join(
|
|
2134
|
+
const packPath = join(dbRoot, "packs", packName);
|
|
2035
2135
|
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
2036
2136
|
if (!existsSync(dbPath) && !existsSync(doltDir)) {
|
|
2037
2137
|
process.stderr.write(`Error: Decision store not initialized. Run 'substrate init' first.\n`);
|
|
@@ -2749,7 +2849,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
2749
2849
|
await initSchema(expAdapter);
|
|
2750
2850
|
const { runRunAction: runPipeline } = await import(
|
|
2751
2851
|
/* @vite-ignore */
|
|
2752
|
-
"../run-
|
|
2852
|
+
"../run-B-TUWMCv.js"
|
|
2753
2853
|
);
|
|
2754
2854
|
const runStoryFn = async (opts) => {
|
|
2755
2855
|
const exitCode = await runPipeline({
|
|
@@ -7142,7 +7242,7 @@ async function runRetryEscalatedAction(options) {
|
|
|
7142
7242
|
}
|
|
7143
7243
|
return 0;
|
|
7144
7244
|
}
|
|
7145
|
-
const packPath = join(
|
|
7245
|
+
const packPath = join(dbRoot, "packs", packName);
|
|
7146
7246
|
const packLoader = createPackLoader();
|
|
7147
7247
|
let pack;
|
|
7148
7248
|
try {
|
|
@@ -7701,6 +7801,261 @@ function registerRoutingCommand(program) {
|
|
|
7701
7801
|
});
|
|
7702
7802
|
}
|
|
7703
7803
|
|
|
7804
|
+
//#endregion
|
|
7805
|
+
//#region src/modules/work-graph/schema.ts
|
|
7806
|
+
/**
|
|
7807
|
+
* Work-graph schema DDL constants.
|
|
7808
|
+
*
|
|
7809
|
+
* Story 31-1 placeholder — defines the `stories`, `story_dependencies`, and
|
|
7810
|
+
* `ready_stories` DDL used by the EpicIngester and downstream consumers.
|
|
7811
|
+
*
|
|
7812
|
+
* NOTE: This file is a minimal placeholder created by story 31-2 because story
|
|
7813
|
+
* 31-1 (schema creation) had not yet run. If story 31-1 produces a richer
|
|
7814
|
+
* schema, merge carefully and remove this note.
|
|
7815
|
+
*/
|
|
7816
|
+
const CREATE_STORIES_TABLE = `
|
|
7817
|
+
CREATE TABLE IF NOT EXISTS stories (
|
|
7818
|
+
story_key VARCHAR(50) NOT NULL,
|
|
7819
|
+
epic_num INT NOT NULL,
|
|
7820
|
+
story_num INT NOT NULL,
|
|
7821
|
+
title VARCHAR(500) NOT NULL,
|
|
7822
|
+
priority VARCHAR(10) NOT NULL,
|
|
7823
|
+
size VARCHAR(50) NOT NULL,
|
|
7824
|
+
sprint INT NOT NULL,
|
|
7825
|
+
status VARCHAR(50) NOT NULL DEFAULT 'planned',
|
|
7826
|
+
PRIMARY KEY (story_key)
|
|
7827
|
+
)
|
|
7828
|
+
`.trim();
|
|
7829
|
+
const CREATE_STORY_DEPENDENCIES_TABLE = `
|
|
7830
|
+
CREATE TABLE IF NOT EXISTS story_dependencies (
|
|
7831
|
+
story_key VARCHAR(50) NOT NULL,
|
|
7832
|
+
depends_on VARCHAR(50) NOT NULL,
|
|
7833
|
+
dependency_type VARCHAR(50) NOT NULL DEFAULT 'blocks',
|
|
7834
|
+
source VARCHAR(50) NOT NULL DEFAULT 'explicit',
|
|
7835
|
+
PRIMARY KEY (story_key, depends_on)
|
|
7836
|
+
)
|
|
7837
|
+
`.trim();
|
|
7838
|
+
const CREATE_READY_STORIES_VIEW = `
|
|
7839
|
+
CREATE VIEW IF NOT EXISTS ready_stories AS
|
|
7840
|
+
SELECT s.*
|
|
7841
|
+
FROM stories s
|
|
7842
|
+
WHERE s.status = 'planned'
|
|
7843
|
+
AND NOT EXISTS (
|
|
7844
|
+
SELECT 1 FROM story_dependencies sd
|
|
7845
|
+
JOIN stories blocking ON sd.depends_on = blocking.story_key
|
|
7846
|
+
WHERE sd.story_key = s.story_key
|
|
7847
|
+
AND blocking.status != 'done'
|
|
7848
|
+
)
|
|
7849
|
+
`.trim();
|
|
7850
|
+
|
|
7851
|
+
//#endregion
|
|
7852
|
+
//#region src/modules/work-graph/epic-parser.ts
|
|
7853
|
+
/** Regex for sprint header lines: `**Sprint 1 —` (em dash or hyphen) */
|
|
7854
|
+
const SPRINT_HEADER_RE = /^\*\*Sprint\s+(\d+)\s*[—–-]/i;
|
|
7855
|
+
/**
|
|
7856
|
+
* Regex for story lines: `- 31-2: Epic doc ingestion (P0, Medium)`
|
|
7857
|
+
* Captures: epicNum, storyNum, title, priority, size
|
|
7858
|
+
*/
|
|
7859
|
+
const STORY_LINE_RE = /^-\s+(\d+)-(\d+):\s+(.+?)\s+\((P\d+),\s+([\w-]+)\)\s*$/;
|
|
7860
|
+
/** Regex to find the story map section heading */
|
|
7861
|
+
const STORY_MAP_HEADING_RE = /^#{1,6}\s+.*Story\s+Map/im;
|
|
7862
|
+
/** Regex to find the dependency chain line */
|
|
7863
|
+
const DEPENDENCY_CHAIN_RE = /\*\*Dependency\s+chain\*\*:\s*(.+)/i;
|
|
7864
|
+
/** Regex for "also gates" clauses: `31-3 also gates 31-6, 31-7` */
|
|
7865
|
+
const ALSO_GATES_RE = /^([\d]+-[\d]+)\s+also\s+gates\s+(.+)$/i;
|
|
7866
|
+
var EpicParser = class {
|
|
7867
|
+
/**
|
|
7868
|
+
* Parse story metadata from an epic planning document.
|
|
7869
|
+
*
|
|
7870
|
+
* @param content - Full text of the epic markdown document.
|
|
7871
|
+
* @returns Array of `ParsedStory` objects, one per story line found.
|
|
7872
|
+
* @throws {Error} If the story map section is absent or no stories can be parsed.
|
|
7873
|
+
*/
|
|
7874
|
+
parseStories(content) {
|
|
7875
|
+
const headingMatch = STORY_MAP_HEADING_RE.exec(content);
|
|
7876
|
+
if (!headingMatch) throw new Error("No story map section found in document");
|
|
7877
|
+
const afterHeading = content.slice(headingMatch.index + headingMatch[0].length);
|
|
7878
|
+
const stories = [];
|
|
7879
|
+
let currentSprint = 0;
|
|
7880
|
+
for (const rawLine of afterHeading.split("\n")) {
|
|
7881
|
+
const line = rawLine.trim();
|
|
7882
|
+
const sprintMatch = SPRINT_HEADER_RE.exec(line);
|
|
7883
|
+
if (sprintMatch) {
|
|
7884
|
+
currentSprint = parseInt(sprintMatch[1], 10);
|
|
7885
|
+
continue;
|
|
7886
|
+
}
|
|
7887
|
+
const storyMatch = STORY_LINE_RE.exec(line);
|
|
7888
|
+
if (storyMatch) {
|
|
7889
|
+
const epicNum = parseInt(storyMatch[1], 10);
|
|
7890
|
+
const storyNum = parseInt(storyMatch[2], 10);
|
|
7891
|
+
stories.push({
|
|
7892
|
+
story_key: `${epicNum}-${storyNum}`,
|
|
7893
|
+
epic_num: epicNum,
|
|
7894
|
+
story_num: storyNum,
|
|
7895
|
+
title: storyMatch[3].trim(),
|
|
7896
|
+
priority: storyMatch[4],
|
|
7897
|
+
size: storyMatch[5],
|
|
7898
|
+
sprint: currentSprint
|
|
7899
|
+
});
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
if (stories.length === 0) throw new Error("Story map section found but contained no parseable story lines");
|
|
7903
|
+
return stories;
|
|
7904
|
+
}
|
|
7905
|
+
/**
|
|
7906
|
+
* Parse dependency relationships from an epic planning document.
|
|
7907
|
+
*
|
|
7908
|
+
* If the `**Dependency chain**:` line is absent, returns an empty array
|
|
7909
|
+
* (not all epics declare dependencies).
|
|
7910
|
+
*
|
|
7911
|
+
* @param content - Full text of the epic markdown document.
|
|
7912
|
+
* @returns Array of `ParsedDependency` objects.
|
|
7913
|
+
*/
|
|
7914
|
+
parseDependencies(content) {
|
|
7915
|
+
const chainLineMatch = DEPENDENCY_CHAIN_RE.exec(content);
|
|
7916
|
+
if (!chainLineMatch) return [];
|
|
7917
|
+
const chainStr = chainLineMatch[1].trim();
|
|
7918
|
+
const dependencies = [];
|
|
7919
|
+
const clauses = chainStr.split(";").map((c) => c.trim()).filter(Boolean);
|
|
7920
|
+
for (const clause of clauses) {
|
|
7921
|
+
const alsoGatesMatch = ALSO_GATES_RE.exec(clause);
|
|
7922
|
+
if (alsoGatesMatch) {
|
|
7923
|
+
const gater = alsoGatesMatch[1].trim();
|
|
7924
|
+
const gatedList = alsoGatesMatch[2].split(",").map((t) => t.trim()).filter(Boolean);
|
|
7925
|
+
for (const gated of gatedList) dependencies.push({
|
|
7926
|
+
story_key: gated,
|
|
7927
|
+
depends_on: gater,
|
|
7928
|
+
dependency_type: "blocks",
|
|
7929
|
+
source: "explicit"
|
|
7930
|
+
});
|
|
7931
|
+
continue;
|
|
7932
|
+
}
|
|
7933
|
+
const parts = clause.split("→").map((p) => p.trim()).filter(Boolean);
|
|
7934
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
7935
|
+
const upstream = parts[i];
|
|
7936
|
+
const downstream = parts[i + 1];
|
|
7937
|
+
dependencies.push({
|
|
7938
|
+
story_key: downstream,
|
|
7939
|
+
depends_on: upstream,
|
|
7940
|
+
dependency_type: "blocks",
|
|
7941
|
+
source: "explicit"
|
|
7942
|
+
});
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
return dependencies;
|
|
7946
|
+
}
|
|
7947
|
+
};
|
|
7948
|
+
|
|
7949
|
+
//#endregion
|
|
7950
|
+
//#region src/modules/work-graph/epic-ingester.ts
|
|
7951
|
+
var EpicIngester = class {
|
|
7952
|
+
adapter;
|
|
7953
|
+
constructor(adapter) {
|
|
7954
|
+
this.adapter = adapter;
|
|
7955
|
+
}
|
|
7956
|
+
/**
|
|
7957
|
+
* Upsert stories and sync dependencies into the database.
|
|
7958
|
+
*
|
|
7959
|
+
* Both operations are wrapped in a single transaction: if either fails the
|
|
7960
|
+
* entire batch is rolled back.
|
|
7961
|
+
*
|
|
7962
|
+
* @param stories - Parsed story metadata from `EpicParser.parseStories()`.
|
|
7963
|
+
* @param dependencies - Parsed dependency edges from `EpicParser.parseDependencies()`.
|
|
7964
|
+
* @returns `IngestResult` with counts of affected rows.
|
|
7965
|
+
*/
|
|
7966
|
+
async ingest(stories, dependencies) {
|
|
7967
|
+
return this.adapter.transaction(async (tx) => {
|
|
7968
|
+
let storiesUpserted = 0;
|
|
7969
|
+
for (const story of stories) {
|
|
7970
|
+
const existing = await tx.query("SELECT status FROM stories WHERE story_key = ?", [story.story_key]);
|
|
7971
|
+
if (existing.length > 0) await tx.query("UPDATE stories SET title = ?, priority = ?, size = ?, sprint = ? WHERE story_key = ?", [
|
|
7972
|
+
story.title,
|
|
7973
|
+
story.priority,
|
|
7974
|
+
story.size,
|
|
7975
|
+
story.sprint,
|
|
7976
|
+
story.story_key
|
|
7977
|
+
]);
|
|
7978
|
+
else {
|
|
7979
|
+
await tx.query("INSERT INTO stories (story_key, epic_num, story_num, title, priority, size, sprint, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
7980
|
+
story.story_key,
|
|
7981
|
+
story.epic_num,
|
|
7982
|
+
story.story_num,
|
|
7983
|
+
story.title,
|
|
7984
|
+
story.priority,
|
|
7985
|
+
story.size,
|
|
7986
|
+
story.sprint,
|
|
7987
|
+
"planned"
|
|
7988
|
+
]);
|
|
7989
|
+
storiesUpserted++;
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
const epicNum = stories.length > 0 ? stories[0].epic_num : null;
|
|
7993
|
+
if (epicNum !== null) await tx.query(`DELETE FROM story_dependencies WHERE source = 'explicit' AND story_key LIKE ?`, [`${epicNum}-%`]);
|
|
7994
|
+
for (const dep of dependencies) await tx.query("INSERT INTO story_dependencies (story_key, depends_on, dependency_type, source) VALUES (?, ?, ?, ?)", [
|
|
7995
|
+
dep.story_key,
|
|
7996
|
+
dep.depends_on,
|
|
7997
|
+
dep.dependency_type,
|
|
7998
|
+
dep.source
|
|
7999
|
+
]);
|
|
8000
|
+
return {
|
|
8001
|
+
storiesUpserted,
|
|
8002
|
+
dependenciesReplaced: dependencies.length
|
|
8003
|
+
};
|
|
8004
|
+
});
|
|
8005
|
+
}
|
|
8006
|
+
};
|
|
8007
|
+
|
|
8008
|
+
//#endregion
|
|
8009
|
+
//#region src/cli/commands/ingest-epic.ts
|
|
8010
|
+
function registerIngestEpicCommand(program) {
|
|
8011
|
+
program.command("ingest-epic <epic-doc-path>").description("Parse an epic planning doc and upsert story metadata into the work-graph").action(async (epicDocPath) => {
|
|
8012
|
+
if (!existsSync$1(epicDocPath)) {
|
|
8013
|
+
process.stderr.write(`Error: File not found: ${epicDocPath}\n`);
|
|
8014
|
+
process.exitCode = 1;
|
|
8015
|
+
return;
|
|
8016
|
+
}
|
|
8017
|
+
let content;
|
|
8018
|
+
try {
|
|
8019
|
+
content = readFileSync$1(epicDocPath, "utf-8");
|
|
8020
|
+
} catch (err) {
|
|
8021
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8022
|
+
process.stderr.write(`Error: Cannot read file ${epicDocPath}: ${msg}\n`);
|
|
8023
|
+
process.exitCode = 1;
|
|
8024
|
+
return;
|
|
8025
|
+
}
|
|
8026
|
+
const parser = new EpicParser();
|
|
8027
|
+
let stories;
|
|
8028
|
+
let dependencies;
|
|
8029
|
+
try {
|
|
8030
|
+
stories = parser.parseStories(content);
|
|
8031
|
+
dependencies = parser.parseDependencies(content);
|
|
8032
|
+
} catch (err) {
|
|
8033
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8034
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
8035
|
+
process.exitCode = 1;
|
|
8036
|
+
return;
|
|
8037
|
+
}
|
|
8038
|
+
const adapter = createDatabaseAdapter({
|
|
8039
|
+
backend: "auto",
|
|
8040
|
+
basePath: process.cwd()
|
|
8041
|
+
});
|
|
8042
|
+
try {
|
|
8043
|
+
await adapter.exec(CREATE_STORIES_TABLE);
|
|
8044
|
+
await adapter.exec(CREATE_STORY_DEPENDENCIES_TABLE);
|
|
8045
|
+
const ingester = new EpicIngester(adapter);
|
|
8046
|
+
const result = await ingester.ingest(stories, dependencies);
|
|
8047
|
+
const epicNum = stories[0].epic_num;
|
|
8048
|
+
process.stdout.write(`Ingested ${result.storiesUpserted} stories and ${result.dependenciesReplaced} dependencies from epic ${epicNum}\n`);
|
|
8049
|
+
} catch (err) {
|
|
8050
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8051
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
8052
|
+
process.exitCode = 1;
|
|
8053
|
+
} finally {
|
|
8054
|
+
await adapter.close();
|
|
8055
|
+
}
|
|
8056
|
+
});
|
|
8057
|
+
}
|
|
8058
|
+
|
|
7704
8059
|
//#endregion
|
|
7705
8060
|
//#region src/cli/index.ts
|
|
7706
8061
|
process.setMaxListeners(20);
|
|
@@ -7761,6 +8116,7 @@ async function createProgram() {
|
|
|
7761
8116
|
registerWorktreesCommand(program, version);
|
|
7762
8117
|
registerBrainstormCommand(program, version);
|
|
7763
8118
|
registerExportCommand(program, version);
|
|
8119
|
+
registerIngestEpicCommand(program);
|
|
7764
8120
|
registerUpgradeCommand(program);
|
|
7765
8121
|
return program;
|
|
7766
8122
|
}
|
|
@@ -247,7 +247,8 @@ var InMemoryDatabaseAdapter = class {
|
|
|
247
247
|
const upper = resolved.trimStart().toUpperCase();
|
|
248
248
|
if (/^CREATE\s+TABLE/i.test(upper)) return this._createTable(resolved);
|
|
249
249
|
if (/^DROP\s+TABLE/i.test(upper)) return this._dropTable(resolved);
|
|
250
|
-
if (/^
|
|
250
|
+
if (/^CREATE\s+(?:OR\s+REPLACE\s+)?VIEW/i.test(upper)) return [];
|
|
251
|
+
if (/^INSERT\s+(?:IGNORE\s+)?INTO/i.test(upper)) return this._insert(resolved, /^INSERT\s+IGNORE\s+INTO/i.test(upper));
|
|
251
252
|
if (/^SELECT/i.test(upper)) return this._select(resolved);
|
|
252
253
|
if (/^UPDATE/i.test(upper)) return this._update(resolved);
|
|
253
254
|
if (/^DELETE\s+FROM/i.test(upper)) return this._delete(resolved);
|
|
@@ -539,6 +540,8 @@ var DoltClient = class {
|
|
|
539
540
|
_pool = null;
|
|
540
541
|
_useCliMode = false;
|
|
541
542
|
_connected = false;
|
|
543
|
+
/** Promise-chain mutex that serializes all CLI operations to prevent concurrent noms manifest access */
|
|
544
|
+
_cliMutex = Promise.resolve();
|
|
542
545
|
constructor(options) {
|
|
543
546
|
this.repoPath = options.repoPath;
|
|
544
547
|
this.socketPath = options.socketPath ?? `${options.repoPath}/.dolt/dolt.sock`;
|
|
@@ -586,6 +589,20 @@ var DoltClient = class {
|
|
|
586
589
|
throw new DoltQueryError(sql, detail);
|
|
587
590
|
}
|
|
588
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* Acquire an exclusive CLI lock. Dolt CLI takes an exclusive lock on the noms
|
|
594
|
+
* manifest, so concurrent `dolt sql -q` / `dolt <subcommand>` processes
|
|
595
|
+
* produce "cannot update manifest: database is read only" errors.
|
|
596
|
+
* Serialize all CLI operations through a single promise chain.
|
|
597
|
+
*/
|
|
598
|
+
_withCliLock(fn) {
|
|
599
|
+
const prev = this._cliMutex;
|
|
600
|
+
let release;
|
|
601
|
+
this._cliMutex = new Promise((resolve$2) => {
|
|
602
|
+
release = resolve$2;
|
|
603
|
+
});
|
|
604
|
+
return prev.then(fn).finally(() => release());
|
|
605
|
+
}
|
|
589
606
|
async _queryCli(sql, params, branch) {
|
|
590
607
|
let resolvedSql = sql;
|
|
591
608
|
if (params && params.length > 0) {
|
|
@@ -597,24 +614,27 @@ var DoltClient = class {
|
|
|
597
614
|
return `'${String(val).replace(/'/g, "''")}'`;
|
|
598
615
|
});
|
|
599
616
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
"
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
const finalSql = resolvedSql;
|
|
618
|
+
return this._withCliLock(async () => {
|
|
619
|
+
try {
|
|
620
|
+
const branchPrefix = branch ? `CALL DOLT_CHECKOUT('${branch.replace(/'/g, "''")}'); ` : "";
|
|
621
|
+
const args = [
|
|
622
|
+
"sql",
|
|
623
|
+
"-q",
|
|
624
|
+
branchPrefix + finalSql,
|
|
625
|
+
"--result-format",
|
|
626
|
+
"json"
|
|
627
|
+
];
|
|
628
|
+
const { stdout } = await runExecFile("dolt", args, { cwd: this.repoPath });
|
|
629
|
+
const lines = (stdout || "").trim().split("\n").filter(Boolean);
|
|
630
|
+
const lastLine = lines.length > 0 ? lines[lines.length - 1] : "{\"rows\":[]}";
|
|
631
|
+
const parsed = JSON.parse(lastLine);
|
|
632
|
+
return parsed.rows ?? [];
|
|
633
|
+
} catch (err) {
|
|
634
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
635
|
+
throw new DoltQueryError(finalSql, detail);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
618
638
|
}
|
|
619
639
|
/**
|
|
620
640
|
* Execute a raw Dolt CLI command (e.g. `dolt diff main...story/26-1 --stat`)
|
|
@@ -635,13 +655,15 @@ var DoltClient = class {
|
|
|
635
655
|
* messages) to avoid whitespace-splitting issues.
|
|
636
656
|
*/
|
|
637
657
|
async execArgs(args) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
658
|
+
return this._withCliLock(async () => {
|
|
659
|
+
try {
|
|
660
|
+
const { stdout } = await runExecFile("dolt", args, { cwd: this.repoPath });
|
|
661
|
+
return stdout;
|
|
662
|
+
} catch (err) {
|
|
663
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
664
|
+
throw new DoltQueryError(args.join(" "), detail);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
645
667
|
}
|
|
646
668
|
async close() {
|
|
647
669
|
if (this._pool) {
|
|
@@ -17004,7 +17026,12 @@ var PhaseOrchestratorImpl = class {
|
|
|
17004
17026
|
if (result.passed) lastCompletedPhaseIdx = i;
|
|
17005
17027
|
else break;
|
|
17006
17028
|
}
|
|
17007
|
-
const
|
|
17029
|
+
const gateDerivedResumeIdx = lastCompletedPhaseIdx + 1;
|
|
17030
|
+
let resumePhaseIdx = gateDerivedResumeIdx;
|
|
17031
|
+
if (run.current_phase) {
|
|
17032
|
+
const dbPhaseIdx = this._phases.findIndex((p) => p.name === run.current_phase);
|
|
17033
|
+
if (dbPhaseIdx >= 0 && dbPhaseIdx > gateDerivedResumeIdx) resumePhaseIdx = dbPhaseIdx;
|
|
17034
|
+
}
|
|
17008
17035
|
if (resumePhaseIdx >= this._phases.length) {
|
|
17009
17036
|
await updatePipelineRun(this._db, runId, { status: "completed" });
|
|
17010
17037
|
return this.getRunStatus(runId);
|
|
@@ -20588,8 +20615,8 @@ async function runRunAction(options) {
|
|
|
20588
20615
|
return 1;
|
|
20589
20616
|
}
|
|
20590
20617
|
}
|
|
20591
|
-
const packPath = join(projectRoot, "packs", packName);
|
|
20592
20618
|
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
20619
|
+
const packPath = join(dbRoot, "packs", packName);
|
|
20593
20620
|
const dbDir = join(dbRoot, ".substrate");
|
|
20594
20621
|
const dbPath = join(dbDir, "substrate.db");
|
|
20595
20622
|
mkdirSync(dbDir, { recursive: true });
|
|
@@ -21723,5 +21750,5 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
21723
21750
|
}
|
|
21724
21751
|
|
|
21725
21752
|
//#endregion
|
|
21726
|
-
export { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
21727
|
-
//# sourceMappingURL=run-
|
|
21753
|
+
export { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
21754
|
+
//# sourceMappingURL=run-BD0Ugp7F.js.map
|
package/dist/schema.sql
CHANGED
|
@@ -236,3 +236,48 @@ CREATE TABLE IF NOT EXISTS repo_map_meta (
|
|
|
236
236
|
|
|
237
237
|
INSERT IGNORE INTO _schema_version (version, description) VALUES (5, 'Add repo_map_symbols and repo_map_meta tables (Epic 28-2)');
|
|
238
238
|
INSERT IGNORE INTO _schema_version (version, description) VALUES (6, 'Add dependencies JSON column to repo_map_symbols (Epic 28-3)');
|
|
239
|
+
|
|
240
|
+
-- ---------------------------------------------------------------------------
|
|
241
|
+
-- wg_stories (Epic 31-1) — planning-level work graph story nodes
|
|
242
|
+
-- ---------------------------------------------------------------------------
|
|
243
|
+
CREATE TABLE IF NOT EXISTS wg_stories (
|
|
244
|
+
story_key VARCHAR(20) NOT NULL,
|
|
245
|
+
epic VARCHAR(20) NOT NULL,
|
|
246
|
+
title VARCHAR(255),
|
|
247
|
+
status VARCHAR(30) NOT NULL DEFAULT 'planned',
|
|
248
|
+
spec_path VARCHAR(500),
|
|
249
|
+
created_at DATETIME,
|
|
250
|
+
updated_at DATETIME,
|
|
251
|
+
completed_at DATETIME,
|
|
252
|
+
PRIMARY KEY (story_key)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_wg_stories_epic ON wg_stories (epic);
|
|
256
|
+
|
|
257
|
+
-- ---------------------------------------------------------------------------
|
|
258
|
+
-- story_dependencies (Epic 31-1) — directed dependency edges
|
|
259
|
+
-- ---------------------------------------------------------------------------
|
|
260
|
+
CREATE TABLE IF NOT EXISTS story_dependencies (
|
|
261
|
+
story_key VARCHAR(20) NOT NULL,
|
|
262
|
+
depends_on VARCHAR(20) NOT NULL,
|
|
263
|
+
dep_type VARCHAR(20) NOT NULL,
|
|
264
|
+
source VARCHAR(20) NOT NULL,
|
|
265
|
+
created_at DATETIME,
|
|
266
|
+
PRIMARY KEY (story_key, depends_on)
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
-- ---------------------------------------------------------------------------
|
|
270
|
+
-- ready_stories view (Epic 31-1)
|
|
271
|
+
-- ---------------------------------------------------------------------------
|
|
272
|
+
CREATE OR REPLACE VIEW ready_stories AS
|
|
273
|
+
SELECT s.* FROM wg_stories s
|
|
274
|
+
WHERE s.status IN ('planned', 'ready')
|
|
275
|
+
AND NOT EXISTS (
|
|
276
|
+
SELECT 1 FROM story_dependencies d
|
|
277
|
+
JOIN wg_stories dep ON dep.story_key = d.depends_on
|
|
278
|
+
WHERE d.story_key = s.story_key
|
|
279
|
+
AND d.dep_type = 'blocks'
|
|
280
|
+
AND dep.status <> 'complete'
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
INSERT IGNORE INTO _schema_version (version, description) VALUES (7, 'Add wg_stories, story_dependencies tables and ready_stories view (Epic 31-1)');
|