ROADMAP.md

This commit is contained in:
2026-01-09 21:36:11 +00:00
parent 42ec8166a9
commit 35d27a44e1
13 changed files with 324 additions and 49 deletions

View File

@@ -289,8 +289,11 @@ set(WORKFLOW_SOURCES
src/services/impl/workflow_step_registry.cpp
src/services/impl/workflow_executor.cpp
src/services/impl/workflow_definition_parser.cpp
src/services/impl/workflow_template_resolver.cpp
src/services/impl/workflow_config_pipeline.cpp
src/services/impl/workflow_config_load_step.cpp
src/services/impl/workflow_config_version_step.cpp
src/services/impl/workflow_config_migration_step.cpp
src/services/impl/workflow_config_schema_step.cpp
src/services/impl/workflow_default_step_registrar.cpp
)
@@ -579,6 +582,7 @@ add_executable(vulkan_shader_linking_test
src/services/impl/bgfx_gui_service.cpp
src/services/impl/bgfx_shader_compiler.cpp
${JSON_CONFIG_SOURCES}
${WORKFLOW_SOURCES}
src/services/impl/materialx_shader_generator.cpp
src/services/impl/shader_pipeline_validator.cpp
src/services/impl/platform_service.cpp
@@ -767,6 +771,7 @@ add_test(NAME render_graph_service_test COMMAND render_graph_service_test)
add_executable(json_config_merge_test
tests/json_config_merge_test.cpp
${JSON_CONFIG_SOURCES}
${WORKFLOW_SOURCES}
)
target_include_directories(json_config_merge_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(json_config_merge_test PRIVATE
@@ -780,6 +785,7 @@ add_test(NAME json_config_merge_test COMMAND json_config_merge_test)
add_executable(json_config_schema_validation_test
tests/json_config_schema_validation_test.cpp
${JSON_CONFIG_SOURCES}
${WORKFLOW_SOURCES}
)
target_include_directories(json_config_schema_validation_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(json_config_schema_validation_test PRIVATE

View File

@@ -237,11 +237,12 @@ Option B: per-shader only
### Status
- [~] Workflow core: step registry + executor + JSON definition parser.
- [~] Default step package: `config.load`, `config.version.validate`, `config.schema.validate`.
- [~] Boot config workflow execution (load/version/migrate/schema); runtime config parsing still outside workflow.
- [x] Workflow schema: `config/schema/workflow_v1.schema.json`.
- [x] Template package: `config/workflows/templates/boot_default.json`.
### Next Steps
- Wire boot pipeline to use workflow executor (config load/validate/migrate).
- Move RuntimeConfig parsing into a workflow step.
- Add frame workflow template (BeginFrame → RenderGraph → Capture → Validate).
## Feature Matrix (What You Get, When You Get It)

View File

@@ -22,6 +22,19 @@
"version": "config.version"
}
},
{
"id": "migrate_version",
"plugin": "config.migrate",
"inputs": {
"document": "config.document",
"path": "config.path",
"version": "config.version"
},
"outputs": {
"document": "config.document",
"version": "config.version"
}
},
{
"id": "validate_schema",
"plugin": "config.schema.validate",

View File

@@ -1,9 +1,6 @@
#include "json_config_service.hpp"
#include "json_config_document_loader.hpp"
#include "json_config_migration_service.hpp"
#include "json_config_schema_validator.hpp"
#include "json_config_schema_version.hpp"
#include "json_config_version_validator.hpp"
#include "workflow_config_pipeline.hpp"
#include "../interfaces/i_logger.hpp"
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
@@ -120,27 +117,12 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
", dumpConfig=" + (dumpConfig ? "true" : "false");
logger->Trace("JsonConfigService", "LoadFromJson", args);
json_config::JsonConfigDocumentLoader documentLoader(logger);
rapidjson::Document document = documentLoader.Load(configPath);
json_config::JsonConfigVersionValidator versionValidator(logger);
const auto activeVersion = versionValidator.Validate(document, configPath);
if (activeVersion && *activeVersion != json_config::kRuntimeConfigSchemaVersion) {
json_config::JsonConfigMigrationService migrationService(logger, probeService);
const bool migrated = migrationService.Apply(document,
*activeVersion,
json_config::kRuntimeConfigSchemaVersion,
configPath);
if (!migrated) {
throw std::runtime_error("Unsupported schema version " + std::to_string(*activeVersion) +
" in " + configPath.string() +
"; expected " + std::to_string(json_config::kRuntimeConfigSchemaVersion) +
" (see config/schema/MIGRATIONS.md)");
}
WorkflowConfigPipeline pipeline(logger, probeService);
std::shared_ptr<rapidjson::Document> documentHandle = pipeline.Execute(configPath, nullptr);
if (!documentHandle) {
throw std::runtime_error("JsonConfigService::LoadFromJson: workflow pipeline returned null document");
}
json_config::JsonConfigSchemaValidator schemaValidator(logger, probeService);
schemaValidator.ValidateOrThrow(document, configPath);
const rapidjson::Document& document = *documentHandle;
if (dumpConfig || configJson) {
rapidjson::StringBuffer buffer;

View File

@@ -0,0 +1,92 @@
#include "workflow_config_migration_step.hpp"
#include "json_config_migration_service.hpp"
#include "json_config_schema_version.hpp"
#include "workflow_step_io_resolver.hpp"
#include <rapidjson/document.h>
#include <filesystem>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
namespace sdl3cpp::services::impl {
WorkflowConfigMigrationStep::WorkflowConfigMigrationStep(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService)
: logger_(std::move(logger)),
probeService_(std::move(probeService)) {}
std::string WorkflowConfigMigrationStep::GetPluginId() const {
return "config.migrate";
}
void WorkflowConfigMigrationStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
WorkflowStepIoResolver resolver;
const std::string documentKey = resolver.GetRequiredInputKey(step, "document");
const std::string pathKey = resolver.GetRequiredInputKey(step, "path");
const std::string versionKey = resolver.GetRequiredInputKey(step, "version");
const std::string outputDocumentKey = resolver.GetRequiredOutputKey(step, "document");
const std::string outputVersionKey = resolver.GetRequiredOutputKey(step, "version");
const auto* documentHandle = context.TryGet<std::shared_ptr<rapidjson::Document>>(documentKey);
if (!documentHandle || !(*documentHandle)) {
throw std::runtime_error("Workflow config.migrate missing document input '" + documentKey + "'");
}
std::shared_ptr<rapidjson::Document> document = *documentHandle;
const auto* versionHandle = context.TryGet<std::optional<int>>(versionKey);
if (!versionHandle) {
throw std::runtime_error("Workflow config.migrate missing version input '" + versionKey + "'");
}
std::optional<int> version = *versionHandle;
std::filesystem::path pathValue;
if (const auto* path = context.TryGet<std::filesystem::path>(pathKey)) {
pathValue = *path;
} else if (const auto* pathString = context.TryGet<std::string>(pathKey)) {
pathValue = *pathString;
} else {
throw std::runtime_error("Workflow config.migrate missing path input '" + pathKey + "'");
}
if (!version) {
if (logger_) {
logger_->Trace("WorkflowConfigMigrationStep", "Execute",
"configPath=" + pathValue.string(),
"No schema version provided; skipping migration");
}
} else if (*version == json_config::kRuntimeConfigSchemaVersion) {
if (logger_) {
logger_->Trace("WorkflowConfigMigrationStep", "Execute",
"version=" + std::to_string(*version),
"Schema version matches runtime; skipping migration");
}
} else {
if (logger_) {
logger_->Info("WorkflowConfigMigrationStep: Migrating config from version " +
std::to_string(*version) + " to " +
std::to_string(json_config::kRuntimeConfigSchemaVersion));
}
json_config::JsonConfigMigrationService migrationService(logger_, probeService_);
const bool migrated = migrationService.Apply(*document,
*version,
json_config::kRuntimeConfigSchemaVersion,
pathValue);
if (!migrated) {
throw std::runtime_error("Unsupported schema version " + std::to_string(*version) +
" in " + pathValue.string() +
"; expected " + std::to_string(json_config::kRuntimeConfigSchemaVersion) +
" (see config/schema/MIGRATIONS.md)");
}
version = json_config::kRuntimeConfigSchemaVersion;
}
context.Set(outputDocumentKey, document);
context.Set(outputVersionKey, version);
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,24 @@
#pragma once
#include "../interfaces/i_workflow_step.hpp"
#include "../interfaces/i_logger.hpp"
#include "../interfaces/i_probe_service.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
class WorkflowConfigMigrationStep final : public IWorkflowStep {
public:
WorkflowConfigMigrationStep(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService);
std::string GetPluginId() const override;
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
private:
std::shared_ptr<ILogger> logger_;
std::shared_ptr<IProbeService> probeService_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,68 @@
#include "workflow_config_pipeline.hpp"
#include "workflow_default_step_registrar.hpp"
#include "workflow_definition_parser.hpp"
#include "workflow_executor.hpp"
#include "workflow_step_registry.hpp"
#include "workflow_template_resolver.hpp"
#include "../interfaces/i_logger.hpp"
#include "../interfaces/i_probe_service.hpp"
#include <stdexcept>
#include <string>
#include <utility>
namespace sdl3cpp::services::impl {
WorkflowConfigPipeline::WorkflowConfigPipeline(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService)
: logger_(std::move(logger)),
probeService_(std::move(probeService)) {}
std::shared_ptr<rapidjson::Document> WorkflowConfigPipeline::Execute(
const std::filesystem::path& configPath,
std::optional<int>* versionOut) const {
if (logger_) {
logger_->Trace("WorkflowConfigPipeline", "Execute",
"configPath=" + configPath.string(),
"Starting boot workflow");
}
WorkflowTemplateResolver resolver;
const std::filesystem::path templatePath = resolver.ResolveBootTemplate(configPath);
if (templatePath.empty()) {
throw std::runtime_error("WorkflowConfigPipeline: boot workflow template not found for " +
configPath.string());
}
WorkflowDefinitionParser parser;
const WorkflowDefinition workflow = parser.ParseFile(templatePath);
auto registry = std::make_shared<WorkflowStepRegistry>();
WorkflowDefaultStepRegistrar registrar(logger_, probeService_);
registrar.RegisterUsedSteps(workflow, registry);
WorkflowExecutor executor(registry, logger_);
WorkflowContext context;
context.Set("config.path", configPath);
executor.Execute(workflow, context);
const auto* documentHandle = context.TryGet<std::shared_ptr<rapidjson::Document>>("config.document");
if (!documentHandle || !(*documentHandle)) {
throw std::runtime_error("WorkflowConfigPipeline: boot workflow did not provide config.document");
}
if (versionOut) {
const auto* versionHandle = context.TryGet<std::optional<int>>("config.version");
*versionOut = versionHandle ? *versionHandle : std::nullopt;
}
if (logger_) {
logger_->Trace("WorkflowConfigPipeline", "Execute",
"templatePath=" + templatePath.string(),
"Boot workflow complete");
}
return *documentHandle;
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,29 @@
#pragma once
#include <filesystem>
#include <memory>
#include <optional>
#include <rapidjson/document.h>
namespace sdl3cpp::services {
class ILogger;
class IProbeService;
}
namespace sdl3cpp::services::impl {
class WorkflowConfigPipeline {
public:
WorkflowConfigPipeline(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService);
std::shared_ptr<rapidjson::Document> Execute(const std::filesystem::path& configPath,
std::optional<int>* versionOut) const;
private:
std::shared_ptr<ILogger> logger_;
std::shared_ptr<IProbeService> probeService_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -1,5 +1,6 @@
#include "workflow_default_step_registrar.hpp"
#include "workflow_config_load_step.hpp"
#include "workflow_config_migration_step.hpp"
#include "workflow_config_schema_step.hpp"
#include "workflow_config_version_step.hpp"
@@ -32,6 +33,9 @@ void WorkflowDefaultStepRegistrar::RegisterUsedSteps(
if (plugins.contains("config.version.validate")) {
registry->RegisterStep(std::make_shared<WorkflowConfigVersionStep>(logger_));
}
if (plugins.contains("config.migrate")) {
registry->RegisterStep(std::make_shared<WorkflowConfigMigrationStep>(logger_, probeService_));
}
if (plugins.contains("config.schema.validate")) {
registry->RegisterStep(std::make_shared<WorkflowConfigSchemaStep>(logger_, probeService_));
}

View File

@@ -0,0 +1,26 @@
#include "workflow_template_resolver.hpp"
#include <system_error>
#include <vector>
namespace sdl3cpp::services::impl {
std::filesystem::path WorkflowTemplateResolver::ResolveBootTemplate(
const std::filesystem::path& configPath) const {
const std::filesystem::path templateRelative = "workflows/templates/boot_default.json";
std::vector<std::filesystem::path> candidates;
if (!configPath.empty()) {
candidates.push_back(configPath.parent_path() / templateRelative);
}
candidates.push_back(std::filesystem::current_path() / "config" / templateRelative);
std::error_code ec;
for (const auto& candidate : candidates) {
if (!candidate.empty() && std::filesystem::exists(candidate, ec)) {
return candidate;
}
}
return {};
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,12 @@
#pragma once
#include <filesystem>
namespace sdl3cpp::services::impl {
class WorkflowTemplateResolver {
public:
std::filesystem::path ResolveBootTemplate(const std::filesystem::path& configPath) const;
};
} // namespace sdl3cpp::services::impl

View File

@@ -3,10 +3,12 @@
#include "services/impl/json_config_service.hpp"
#include "services/interfaces/i_logger.hpp"
#include <array>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
#include <utility>
namespace {
@@ -67,15 +69,22 @@ void WriteFile(const std::filesystem::path& path, const std::string& contents) {
output << contents;
}
void CopySchema(const std::filesystem::path& targetDir) {
auto schemaSource = GetRepoRoot() / "config" / "schema" / "runtime_config_v2.schema.json";
auto schemaTarget = targetDir / "schema" / "runtime_config_v2.schema.json";
std::filesystem::create_directories(schemaTarget.parent_path());
std::ifstream input(schemaSource);
ASSERT_TRUE(input.is_open()) << "Missing schema source: " << schemaSource;
std::ofstream output(schemaTarget);
ASSERT_TRUE(output.is_open()) << "Failed to write schema target: " << schemaTarget;
output << input.rdbuf();
void CopyConfigAssets(const std::filesystem::path& targetDir) {
const auto repoRoot = GetRepoRoot();
const std::array<std::pair<std::filesystem::path, std::filesystem::path>, 2> assets = {
std::make_pair(repoRoot / "config" / "schema" / "runtime_config_v2.schema.json",
targetDir / "schema" / "runtime_config_v2.schema.json"),
std::make_pair(repoRoot / "config" / "workflows" / "templates" / "boot_default.json",
targetDir / "workflows" / "templates" / "boot_default.json")
};
for (const auto& asset : assets) {
std::filesystem::create_directories(asset.second.parent_path());
std::ifstream input(asset.first);
ASSERT_TRUE(input.is_open()) << "Missing config asset: " << asset.first;
std::ofstream output(asset.second);
ASSERT_TRUE(output.is_open()) << "Failed to write config asset: " << asset.second;
output << input.rdbuf();
}
}
std::filesystem::path WriteLuaScript(const std::filesystem::path& rootDir) {
@@ -86,7 +95,7 @@ std::filesystem::path WriteLuaScript(const std::filesystem::path& rootDir) {
TEST(JsonConfigMergeTest, OverlayOverridesBaseFields) {
ScopedTempDir tempDir;
CopySchema(tempDir.Path());
CopyConfigAssets(tempDir.Path());
WriteLuaScript(tempDir.Path());
auto logger = std::make_shared<NullLogger>();
@@ -117,7 +126,7 @@ TEST(JsonConfigMergeTest, OverlayOverridesBaseFields) {
TEST(JsonConfigMergeTest, DeleteDirectiveRemovesObject) {
ScopedTempDir tempDir;
CopySchema(tempDir.Path());
CopyConfigAssets(tempDir.Path());
WriteLuaScript(tempDir.Path());
auto logger = std::make_shared<NullLogger>();
@@ -154,7 +163,7 @@ TEST(JsonConfigMergeTest, DeleteDirectiveRemovesObject) {
TEST(JsonConfigMergeTest, ExtendsArrayAppliesInOrder) {
ScopedTempDir tempDir;
CopySchema(tempDir.Path());
CopyConfigAssets(tempDir.Path());
WriteLuaScript(tempDir.Path());
auto logger = std::make_shared<NullLogger>();
@@ -190,7 +199,7 @@ TEST(JsonConfigMergeTest, ExtendsArrayAppliesInOrder) {
TEST(JsonConfigMergeTest, ExtendsCycleThrows) {
ScopedTempDir tempDir;
CopySchema(tempDir.Path());
CopyConfigAssets(tempDir.Path());
WriteLuaScript(tempDir.Path());
auto logger = std::make_shared<NullLogger>();

View File

@@ -3,10 +3,12 @@
#include "services/impl/json_config_service.hpp"
#include "services/interfaces/i_logger.hpp"
#include <array>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
#include <utility>
namespace {
@@ -67,15 +69,22 @@ void WriteFile(const std::filesystem::path& path, const std::string& contents) {
output << contents;
}
void CopySchema(const std::filesystem::path& targetDir) {
auto schemaSource = GetRepoRoot() / "config" / "schema" / "runtime_config_v2.schema.json";
auto schemaTarget = targetDir / "schema" / "runtime_config_v2.schema.json";
std::filesystem::create_directories(schemaTarget.parent_path());
std::ifstream input(schemaSource);
ASSERT_TRUE(input.is_open()) << "Missing schema source: " << schemaSource;
std::ofstream output(schemaTarget);
ASSERT_TRUE(output.is_open()) << "Failed to write schema target: " << schemaTarget;
output << input.rdbuf();
void CopyConfigAssets(const std::filesystem::path& targetDir) {
const auto repoRoot = GetRepoRoot();
const std::array<std::pair<std::filesystem::path, std::filesystem::path>, 2> assets = {
std::make_pair(repoRoot / "config" / "schema" / "runtime_config_v2.schema.json",
targetDir / "schema" / "runtime_config_v2.schema.json"),
std::make_pair(repoRoot / "config" / "workflows" / "templates" / "boot_default.json",
targetDir / "workflows" / "templates" / "boot_default.json")
};
for (const auto& asset : assets) {
std::filesystem::create_directories(asset.second.parent_path());
std::ifstream input(asset.first);
ASSERT_TRUE(input.is_open()) << "Missing config asset: " << asset.first;
std::ofstream output(asset.second);
ASSERT_TRUE(output.is_open()) << "Failed to write config asset: " << asset.second;
output << input.rdbuf();
}
}
void WriteLuaScript(const std::filesystem::path& rootDir) {
@@ -84,7 +93,7 @@ void WriteLuaScript(const std::filesystem::path& rootDir) {
TEST(JsonConfigSchemaValidationTest, RejectsInvalidWindowWidthType) {
ScopedTempDir tempDir;
CopySchema(tempDir.Path());
CopyConfigAssets(tempDir.Path());
WriteLuaScript(tempDir.Path());
auto logger = std::make_shared<NullLogger>();
@@ -104,7 +113,7 @@ TEST(JsonConfigSchemaValidationTest, RejectsInvalidWindowWidthType) {
TEST(JsonConfigSchemaValidationTest, RejectsInvalidSceneSourceEnum) {
ScopedTempDir tempDir;
CopySchema(tempDir.Path());
CopyConfigAssets(tempDir.Path());
WriteLuaScript(tempDir.Path());
auto logger = std::make_shared<NullLogger>();