ROADMAP.md

This commit is contained in:
2026-01-09 20:51:08 +00:00
parent 56e8f240c1
commit 5e805d5c28
27 changed files with 703 additions and 0 deletions

View File

@@ -284,6 +284,17 @@ set(JSON_CONFIG_SOURCES
src/services/impl/json_config_migration_service.cpp
)
set(WORKFLOW_SOURCES
src/services/impl/workflow_step_io_resolver.cpp
src/services/impl/workflow_step_registry.cpp
src/services/impl/workflow_executor.cpp
src/services/impl/workflow_definition_parser.cpp
src/services/impl/workflow_config_load_step.cpp
src/services/impl/workflow_config_version_step.cpp
src/services/impl/workflow_config_schema_step.cpp
src/services/impl/workflow_default_step_registrar.cpp
)
if(BUILD_SDL3_APP)
add_executable(sdl3_app
src/main.cpp
@@ -291,6 +302,7 @@ if(BUILD_SDL3_APP)
src/di/service_registry.cpp
src/events/event_bus.cpp
${JSON_CONFIG_SOURCES}
${WORKFLOW_SOURCES}
src/services/impl/config_compiler_service.cpp
src/services/impl/command_line_service.cpp
src/services/impl/json_config_writer_service.cpp

View File

@@ -226,6 +226,22 @@ Option B: per-shader only
}
```
## Workflow Engine (n8n-Style Micro Steps)
### Goals
- Describe boot + frame pipelines as a declarative JSON workflow graph.
- Keep each step tiny (<100 LOC), with explicit inputs/outputs and DI-backed plugin lookup.
- Package common pipelines as templates so users don't start from scratch.
### Status
- [~] Workflow core: step registry + executor + JSON definition parser.
- [~] Default step package: `config.load`, `config.version.validate`, `config.schema.validate`.
- [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).
- Add frame workflow template (BeginFrame → RenderGraph → Capture → Validate).
## Feature Matrix (What You Get, When You Get It)
| Feature | Status | Starter | Pro | Ultra | Enterprise |
@@ -267,6 +283,7 @@ Option B: per-shader only
- [~] Budget enforcement tests (GUI cache pruning + texture tracker covered; transient pool pending)
- [~] Config-driven validation tour (checkpoint captures + image/ratio/luma/sample-point checks)
- [ ] Smoke test: cube demo boots with config-first scene definition
- [ ] Workflow parser tests (template loading + invalid step diagnostics)
## Test Strategy (Solid Coverage Plan)
### Goals

View File

@@ -0,0 +1,45 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SDL3CPP Workflow v1",
"type": "object",
"properties": {
"template": {
"type": "string"
},
"steps": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"plugin"
],
"properties": {
"id": {
"type": "string"
},
"plugin": {
"type": "string"
},
"inputs": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"outputs": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"additionalProperties": false
}
}
},
"required": [
"steps"
],
"additionalProperties": false
}

View File

@@ -0,0 +1,34 @@
{
"template": "boot.default",
"steps": [
{
"id": "load_config",
"plugin": "config.load",
"inputs": {
"path": "config.path"
},
"outputs": {
"document": "config.document"
}
},
{
"id": "validate_version",
"plugin": "config.version.validate",
"inputs": {
"document": "config.document",
"path": "config.path"
},
"outputs": {
"version": "config.version"
}
},
{
"id": "validate_schema",
"plugin": "config.schema.validate",
"inputs": {
"document": "config.document",
"path": "config.path"
}
}
]
}

View File

@@ -37,11 +37,16 @@
#include "services/impl/logger_service.hpp"
#include "services/impl/pipeline_compiler_service.hpp"
#include "services/impl/validation_tour_service.hpp"
#include "services/impl/workflow_default_step_registrar.hpp"
#include "services/impl/workflow_executor.hpp"
#include "services/impl/workflow_step_registry.hpp"
#include "services/interfaces/i_platform_service.hpp"
#include "services/interfaces/i_probe_service.hpp"
#include "services/interfaces/i_render_graph_service.hpp"
#include "services/interfaces/i_shader_system_registry.hpp"
#include "services/interfaces/i_validation_tour_service.hpp"
#include "services/interfaces/i_workflow_executor.hpp"
#include "services/interfaces/i_workflow_step_registry.hpp"
#include "services/interfaces/i_config_compiler_service.hpp"
#include <iostream>
#include <stdexcept>
@@ -223,6 +228,16 @@ void ServiceBasedApp::RegisterServices() {
registry_.RegisterService<services::IProbeService, services::impl::ProbeService>(
registry_.GetService<services::ILogger>());
// Workflow step registry + executor (declarative boot/frame pipelines)
registry_.RegisterService<services::IWorkflowStepRegistry, services::impl::WorkflowStepRegistry>();
registry_.RegisterService<services::IWorkflowExecutor, services::impl::WorkflowExecutor>(
registry_.GetService<services::IWorkflowStepRegistry>(),
registry_.GetService<services::ILogger>());
services::impl::WorkflowDefaultStepRegistrar workflowRegistrar(
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IProbeService>());
workflowRegistrar.RegisterDefaults(registry_.GetService<services::IWorkflowStepRegistry>());
// Configuration service
registry_.RegisterService<services::IConfigService, services::impl::JsonConfigService>(
registry_.GetService<services::ILogger>(),

View File

@@ -0,0 +1,40 @@
#include "workflow_config_load_step.hpp"
#include "json_config_document_loader.hpp"
#include "workflow_step_io_resolver.hpp"
#include <rapidjson/document.h>
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
namespace sdl3cpp::services::impl {
WorkflowConfigLoadStep::WorkflowConfigLoadStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::string WorkflowConfigLoadStep::GetPluginId() const {
return "config.load";
}
void WorkflowConfigLoadStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
WorkflowStepIoResolver resolver;
const std::string pathKey = resolver.GetRequiredInputKey(step, "path");
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.load missing path input '" + pathKey + "'");
}
json_config::JsonConfigDocumentLoader loader(logger_);
auto document = std::make_shared<rapidjson::Document>(loader.Load(pathValue));
const std::string outputKey = resolver.GetRequiredOutputKey(step, "document");
context.Set(outputKey, std::move(document));
}
} // namespace sdl3cpp::services::impl

View File

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

View File

@@ -0,0 +1,45 @@
#include "workflow_config_schema_step.hpp"
#include "json_config_schema_validator.hpp"
#include "workflow_step_io_resolver.hpp"
#include <rapidjson/document.h>
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
namespace sdl3cpp::services::impl {
WorkflowConfigSchemaStep::WorkflowConfigSchemaStep(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService)
: logger_(std::move(logger)),
probeService_(std::move(probeService)) {}
std::string WorkflowConfigSchemaStep::GetPluginId() const {
return "config.schema.validate";
}
void WorkflowConfigSchemaStep::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 auto* document = context.TryGet<std::shared_ptr<rapidjson::Document>>(documentKey);
if (!document || !(*document)) {
throw std::runtime_error("Workflow config.schema.validate missing document input '" + documentKey + "'");
}
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.schema.validate missing path input '" + pathKey + "'");
}
json_config::JsonConfigSchemaValidator validator(logger_, probeService_);
validator.ValidateOrThrow(**document, pathValue);
}
} // 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 WorkflowConfigSchemaStep : public IWorkflowStep {
public:
WorkflowConfigSchemaStep(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,46 @@
#include "workflow_config_version_step.hpp"
#include "json_config_version_validator.hpp"
#include "workflow_step_io_resolver.hpp"
#include <rapidjson/document.h>
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
namespace sdl3cpp::services::impl {
WorkflowConfigVersionStep::WorkflowConfigVersionStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::string WorkflowConfigVersionStep::GetPluginId() const {
return "config.version.validate";
}
void WorkflowConfigVersionStep::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 auto* document = context.TryGet<std::shared_ptr<rapidjson::Document>>(documentKey);
if (!document || !(*document)) {
throw std::runtime_error("Workflow config.version.validate missing document input '" + documentKey + "'");
}
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.version.validate missing path input '" + pathKey + "'");
}
json_config::JsonConfigVersionValidator validator(logger_);
const auto version = validator.Validate(**document, pathValue);
const std::string outputKey = resolver.GetRequiredOutputKey(step, "version");
context.Set(outputKey, version);
}
} // namespace sdl3cpp::services::impl

View File

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

View File

@@ -0,0 +1,24 @@
#include "workflow_default_step_registrar.hpp"
#include "workflow_config_load_step.hpp"
#include "workflow_config_schema_step.hpp"
#include "workflow_config_version_step.hpp"
#include <stdexcept>
namespace sdl3cpp::services::impl {
WorkflowDefaultStepRegistrar::WorkflowDefaultStepRegistrar(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService)
: logger_(std::move(logger)),
probeService_(std::move(probeService)) {}
void WorkflowDefaultStepRegistrar::RegisterDefaults(const std::shared_ptr<IWorkflowStepRegistry>& registry) const {
if (!registry) {
throw std::runtime_error("WorkflowDefaultStepRegistrar: registry is null");
}
registry->RegisterStep(std::make_shared<WorkflowConfigLoadStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowConfigVersionStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowConfigSchemaStep>(logger_, probeService_));
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,21 @@
#pragma once
#include "../interfaces/i_logger.hpp"
#include "../interfaces/i_probe_service.hpp"
#include "../interfaces/i_workflow_step_registry.hpp"
namespace sdl3cpp::services::impl {
class WorkflowDefaultStepRegistrar {
public:
WorkflowDefaultStepRegistrar(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService);
void RegisterDefaults(const std::shared_ptr<IWorkflowStepRegistry>& registry) const;
private:
std::shared_ptr<ILogger> logger_;
std::shared_ptr<IProbeService> probeService_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,71 @@
#include "workflow_definition_parser.hpp"
#include "json_config_document_parser.hpp"
#include <rapidjson/document.h>
#include <stdexcept>
#include <string>
#include <unordered_map>
namespace sdl3cpp::services::impl {
namespace {
std::string ReadRequiredString(const rapidjson::Value& object, const char* name) {
if (!object.HasMember(name) || !object[name].IsString()) {
throw std::runtime_error("Workflow member '" + std::string(name) + "' must be a string");
}
return object[name].GetString();
}
std::unordered_map<std::string, std::string> ReadStringMap(const rapidjson::Value& object,
const char* name) {
std::unordered_map<std::string, std::string> result;
if (!object.HasMember(name)) {
return result;
}
const auto& mapValue = object[name];
if (!mapValue.IsObject()) {
throw std::runtime_error("Workflow member '" + std::string(name) + "' must be an object");
}
for (auto it = mapValue.MemberBegin(); it != mapValue.MemberEnd(); ++it) {
if (!it->value.IsString()) {
throw std::runtime_error("Workflow map '" + std::string(name) + "' must map to strings");
}
result[it->name.GetString()] = it->value.GetString();
}
return result;
}
} // namespace
WorkflowDefinition WorkflowDefinitionParser::ParseFile(const std::filesystem::path& path) const {
json_config::JsonConfigDocumentParser parser;
rapidjson::Document document = parser.Parse(path, "workflow file");
if (!document.HasMember("steps") || !document["steps"].IsArray()) {
throw std::runtime_error("Workflow must contain a 'steps' array");
}
WorkflowDefinition workflow;
if (document.HasMember("template")) {
if (!document["template"].IsString()) {
throw std::runtime_error("Workflow member 'template' must be a string");
}
workflow.templateName = document["template"].GetString();
}
for (const auto& entry : document["steps"].GetArray()) {
if (!entry.IsObject()) {
throw std::runtime_error("Workflow steps must be objects");
}
WorkflowStepDefinition step;
step.id = ReadRequiredString(entry, "id");
step.plugin = ReadRequiredString(entry, "plugin");
step.inputs = ReadStringMap(entry, "inputs");
step.outputs = ReadStringMap(entry, "outputs");
workflow.steps.push_back(std::move(step));
}
return workflow;
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,14 @@
#pragma once
#include "../interfaces/workflow_definition.hpp"
#include <filesystem>
namespace sdl3cpp::services::impl {
class WorkflowDefinitionParser {
public:
WorkflowDefinition ParseFile(const std::filesystem::path& path) const;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,34 @@
#include "workflow_executor.hpp"
#include <stdexcept>
namespace sdl3cpp::services::impl {
WorkflowExecutor::WorkflowExecutor(std::shared_ptr<IWorkflowStepRegistry> registry,
std::shared_ptr<ILogger> logger)
: registry_(std::move(registry)),
logger_(std::move(logger)) {
if (!registry_) {
throw std::runtime_error("WorkflowExecutor requires a step registry");
}
}
void WorkflowExecutor::Execute(const WorkflowDefinition& workflow, WorkflowContext& context) {
if (logger_) {
logger_->Trace("WorkflowExecutor", "Execute",
"steps=" + std::to_string(workflow.steps.size()),
"Starting workflow execution");
}
for (const auto& step : workflow.steps) {
auto handler = registry_->GetStep(step.plugin);
if (!handler) {
throw std::runtime_error("WorkflowExecutor: no step registered for plugin '" + step.plugin + "'");
}
handler->Execute(step, context);
}
if (logger_) {
logger_->Trace("WorkflowExecutor", "Execute", "", "Workflow execution complete");
}
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,21 @@
#pragma once
#include "../interfaces/i_workflow_executor.hpp"
#include "../interfaces/i_workflow_step_registry.hpp"
#include "../interfaces/i_logger.hpp"
namespace sdl3cpp::services::impl {
class WorkflowExecutor : public IWorkflowExecutor {
public:
WorkflowExecutor(std::shared_ptr<IWorkflowStepRegistry> registry,
std::shared_ptr<ILogger> logger);
void Execute(const WorkflowDefinition& workflow, WorkflowContext& context) override;
private:
std::shared_ptr<IWorkflowStepRegistry> registry_;
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,25 @@
#include "workflow_step_io_resolver.hpp"
#include <stdexcept>
namespace sdl3cpp::services::impl {
std::string WorkflowStepIoResolver::GetRequiredInputKey(const WorkflowStepDefinition& step,
const std::string& name) const {
auto it = step.inputs.find(name);
if (it == step.inputs.end()) {
throw std::runtime_error("Workflow step '" + step.id + "' missing input '" + name + "'");
}
return it->second;
}
std::string WorkflowStepIoResolver::GetRequiredOutputKey(const WorkflowStepDefinition& step,
const std::string& name) const {
auto it = step.outputs.find(name);
if (it == step.outputs.end()) {
throw std::runtime_error("Workflow step '" + step.id + "' missing output '" + name + "'");
}
return it->second;
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,15 @@
#pragma once
#include "../interfaces/workflow_step_definition.hpp"
#include <string>
namespace sdl3cpp::services::impl {
class WorkflowStepIoResolver {
public:
std::string GetRequiredInputKey(const WorkflowStepDefinition& step, const std::string& name) const;
std::string GetRequiredOutputKey(const WorkflowStepDefinition& step, const std::string& name) const;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,26 @@
#include "workflow_step_registry.hpp"
#include <stdexcept>
namespace sdl3cpp::services::impl {
void WorkflowStepRegistry::RegisterStep(std::shared_ptr<IWorkflowStep> step) {
if (!step) {
throw std::runtime_error("WorkflowStepRegistry::RegisterStep: step is null");
}
const std::string pluginId = step->GetPluginId();
auto [it, inserted] = steps_.emplace(pluginId, std::move(step));
if (!inserted) {
throw std::runtime_error("WorkflowStepRegistry::RegisterStep: duplicate plugin '" + pluginId + "'");
}
}
std::shared_ptr<IWorkflowStep> WorkflowStepRegistry::GetStep(const std::string& pluginId) const {
auto it = steps_.find(pluginId);
if (it == steps_.end()) {
return nullptr;
}
return it->second;
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,18 @@
#pragma once
#include "../interfaces/i_workflow_step_registry.hpp"
#include <unordered_map>
namespace sdl3cpp::services::impl {
class WorkflowStepRegistry : public IWorkflowStepRegistry {
public:
void RegisterStep(std::shared_ptr<IWorkflowStep> step) override;
std::shared_ptr<IWorkflowStep> GetStep(const std::string& pluginId) const override;
private:
std::unordered_map<std::string, std::shared_ptr<IWorkflowStep>> steps_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,15 @@
#pragma once
#include "workflow_context.hpp"
#include "workflow_definition.hpp"
namespace sdl3cpp::services {
class IWorkflowExecutor {
public:
virtual ~IWorkflowExecutor() = default;
virtual void Execute(const WorkflowDefinition& workflow, WorkflowContext& context) = 0;
};
} // namespace sdl3cpp::services

View File

@@ -0,0 +1,18 @@
#pragma once
#include "workflow_context.hpp"
#include "workflow_step_definition.hpp"
#include <string>
namespace sdl3cpp::services {
class IWorkflowStep {
public:
virtual ~IWorkflowStep() = default;
virtual std::string GetPluginId() const = 0;
virtual void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) = 0;
};
} // namespace sdl3cpp::services

View File

@@ -0,0 +1,18 @@
#pragma once
#include "i_workflow_step.hpp"
#include <memory>
#include <string>
namespace sdl3cpp::services {
class IWorkflowStepRegistry {
public:
virtual ~IWorkflowStepRegistry() = default;
virtual void RegisterStep(std::shared_ptr<IWorkflowStep> step) = 0;
virtual std::shared_ptr<IWorkflowStep> GetStep(const std::string& pluginId) const = 0;
};
} // namespace sdl3cpp::services

View File

@@ -0,0 +1,33 @@
#pragma once
#include <any>
#include <string>
#include <unordered_map>
namespace sdl3cpp::services {
class WorkflowContext {
public:
template <typename T>
void Set(const std::string& key, T value) {
values_[key] = std::move(value);
}
bool Contains(const std::string& key) const {
return values_.find(key) != values_.end();
}
template <typename T>
const T* TryGet(const std::string& key) const {
auto it = values_.find(key);
if (it == values_.end()) {
return nullptr;
}
return std::any_cast<T>(&it->second);
}
private:
std::unordered_map<std::string, std::any> values_;
};
} // namespace sdl3cpp::services

View File

@@ -0,0 +1,15 @@
#pragma once
#include "workflow_step_definition.hpp"
#include <string>
#include <vector>
namespace sdl3cpp::services {
struct WorkflowDefinition {
std::string templateName;
std::vector<WorkflowStepDefinition> steps;
};
} // namespace sdl3cpp::services

View File

@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <unordered_map>
namespace sdl3cpp::services {
struct WorkflowStepDefinition {
std::string id;
std::string plugin;
std::unordered_map<std::string, std::string> inputs;
std::unordered_map<std::string, std::string> outputs;
};
} // namespace sdl3cpp::services