Checkpoint 5: complete asset freshness, schema migrations and editor diagnostics

This commit is contained in:
Emil
2026-09-18 05:43:02 +03:00
parent e0b965166e
commit 0f34b03631
37 changed files with 1438 additions and 52 deletions
+44 -2
View File
@@ -100,6 +100,25 @@ SchemaRegistry gameplay_schemas(const Json& manifest) {
"Gameplay schema duplicates a builtin or gameplay TypeId: " + id);
result.register_schema(schema);
}
for (const auto& schema : types) {
if (!schema.contains("migrations"))
continue;
require(schema.at("migrations").is_array(), "migration.invalid",
"Migrations must be an array of version steps");
for (const auto& step : schema.at("migrations")) {
require(
step.is_object() && step.contains("from_version") &&
step.at("from_version").is_number_integer() && step.at("from_version") > 0 &&
step.at("from_version") < schema.value("version", 1) &&
step.contains("fields") && step.at("fields").is_object(),
"migration.invalid", "Migration requires a supported earlier version and fields");
for (const auto& [key, unused] : step.items())
require(key == "from_version" || key == "fields", "migration.invalid",
"Unsupported migration step property: " + key);
result.add_migration(schema.at("id"), step.at("from_version").get<int>(),
step.at("fields"));
}
}
return result;
}
bool SchemaRegistry::contains(const std::string& type) const {
@@ -139,9 +158,25 @@ void SchemaRegistry::validate_component(const Json& component) const {
validate_field(value, metadata["fields"][id]);
}
void SchemaRegistry::add_migration(const std::string& type, int from_version, Json rules) {
require(from_version > 0 && rules.is_object(), "migration.invalid", "Invalid migration");
require(contains(type) && from_version > 0 && from_version < schema(type).value("version", 1) &&
rules.is_object(),
"migration.invalid", "Migration requires a registered type and an earlier version");
require(!migrations_.contains({type, from_version}), "migration.duplicate",
"Migration already exists");
for (const auto& [field, rule] : rules.items()) {
require(!field.empty() && rule.is_object(), "migration.invalid",
"Migration fields require nonempty IDs and rule objects");
for (const auto& [operation, value] : rule.items()) {
require(operation == "default" || operation == "scale" || operation == "require_manual",
"migration.invalid", "Unsupported migration operation: " + operation);
if (operation == "scale")
require(value.is_number() && std::isfinite(value.get<double>()),
"migration.invalid", "Migration scale must be a finite number");
else if (operation == "require_manual")
require(value.is_boolean(), "migration.invalid",
"Migration require_manual must be a boolean");
}
}
migrations_[{type, from_version}] = std::move(rules);
}
Json SchemaRegistry::migrate_component(const Json& source) const {
@@ -150,6 +185,10 @@ Json SchemaRegistry::migrate_component(const Json& source) const {
if (!contains(type))
return result;
const auto current = schema(type).value("version", 1);
if (result.contains("version"))
require(result.at("version").is_number_integer() && result.at("version") > 0 &&
result.at("version") <= std::numeric_limits<int>::max(),
"migration.invalid", "Component version must be a positive supported integer");
auto version = result.value("version", 1);
if (version > current)
return result;
@@ -163,8 +202,11 @@ Json SchemaRegistry::migrate_component(const Json& source) const {
if (rule.contains("scale") && result["fields"].contains(field)) {
require(result["fields"][field].is_number(), "migration.type",
"Cannot scale a nonnumeric field");
result["fields"][field] =
const auto scaled =
result["fields"][field].get<double>() * rule["scale"].get<double>();
require(std::isfinite(scaled), "migration.nonfinite",
"Migration produced a non-finite field value");
result["fields"][field] = scaled;
}
if (rule.value("require_manual", false) && result["fields"].contains(field))
throw Error("migration.manual", "Field requires explicit manual migration",
+18 -6
View File
@@ -288,9 +288,6 @@ Json AuthoringService::open(const std::filesystem::path& relative, bool recover)
value.data = recovered.at("scene");
value.revision = recovered.value("revision", 0u);
}
for (auto& item : value.data["entities"])
for (auto& component : item["components"])
component = schemas_.migrate_component(component);
documents_.emplace(id, std::move(value));
return summary(state(id));
}
@@ -375,6 +372,24 @@ void AuthoringService::apply(Json& scene, const Json& command) {
[&](const Json& value) { return value.at("id") == id; });
require(found != values.end(), "component.missing", "Component does not exist");
values.erase(found);
} else if (op == "component.migrate") {
Json* object = nullptr;
if (command.contains("instance")) {
const auto instance_id = command.at("instance").get<std::string>();
for (auto& instance : scene.at("instances"))
if (instance.at("id") == instance_id && instance.contains("additions"))
for (auto& addition : instance.at("additions"))
if (addition.at("id") == command.at("entity"))
object = &addition;
require(object, "template.addition_missing", "Instance-local entity is unavailable");
} else
object = &entity(scene, command.at("entity").get<std::string>());
auto& value = component(*object, command.at("component").get<std::string>());
const auto type = value.at("type").get<std::string>();
require(schemas_.contains(type), "schema.missing", "Component schema unavailable: " + type);
require(value.value("version", 1) <= schemas_.schema(type).value("version", 1),
"migration.future", "Cannot migrate a component from a newer schema");
value = schemas_.migrate_component(value);
} else if (op == "component.set") {
auto& value = component(entity(scene, command.at("entity").get<std::string>()),
command.at("component").get<std::string>());
@@ -601,9 +616,6 @@ Json AuthoringService::recover(const std::string& id,
"recovery.disk_conflict",
"Scene file changed or disappeared since recovery was written");
}
for (auto& item : candidate.data["entities"])
for (auto& component : item["components"])
component = schemas_.migrate_component(component);
validate_scene(candidate.data, schemas_);
if (documents_.contains(id)) {
const auto& current = state(id);
+1 -1
View File
@@ -17,7 +17,7 @@ struct Resolver {
const SceneLoader& loader;
std::string root;
Json conflicts = Json::array();
std::set<std::string> sources;
std::set<std::string> sources{};
void conflict(const Json& path, std::string code, const Json& record) {
conflicts.push_back(
{{"instance_path", path}, {"code", std::move(code)}, {"record", record}});