From 03b5119ef4619b325474dfd6913fe957f159fc38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 03:49:09 +0000 Subject: [PATCH 1/3] Initial plan From c797662711961f2d1bc3235a587355997ba3105c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 03:53:03 +0000 Subject: [PATCH 2/3] feat: implement semantic version comparison for FeatureSchemaV1 Co-authored-by: soficis <107279009+soficis@users.noreply.github.com> --- src/ai/FeatureSchema.cpp | 77 ++++++++++++++++++++++++++++++++- tests/unit/AiExtensionTests.cpp | 34 +++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/ai/FeatureSchema.cpp b/src/ai/FeatureSchema.cpp index aa661c8..5f6c6c3 100644 --- a/src/ai/FeatureSchema.cpp +++ b/src/ai/FeatureSchema.cpp @@ -9,6 +9,55 @@ double vectorValueOrDefault(const std::vector& values, const size_t inde return index < values.size() ? values[index] : 0.0; } +struct SemanticVersion { + int major = 0; + int minor = 0; + int patch = 0; + bool valid = false; +}; + +SemanticVersion parseVersion(const std::string& versionStr) { + SemanticVersion version; + + if (versionStr.empty()) { + return version; + } + + size_t pos = 0; + size_t dotPos = versionStr.find('.'); + + try { + // Parse major version + if (dotPos == std::string::npos) { + version.major = std::stoi(versionStr); + version.valid = true; + return version; + } + + version.major = std::stoi(versionStr.substr(pos, dotPos - pos)); + pos = dotPos + 1; + + // Parse minor version + dotPos = versionStr.find('.', pos); + if (dotPos == std::string::npos) { + version.minor = std::stoi(versionStr.substr(pos)); + version.valid = true; + return version; + } + + version.minor = std::stoi(versionStr.substr(pos, dotPos - pos)); + pos = dotPos + 1; + + // Parse patch version + version.patch = std::stoi(versionStr.substr(pos)); + version.valid = true; + } catch (...) { + version.valid = false; + } + + return version; +} + } // namespace const std::vector& FeatureSchemaV1::names() { @@ -83,7 +132,33 @@ const std::vector& FeatureSchemaV1::names() { return kNames; } -bool FeatureSchemaV1::isCompatible(const std::string& version) { return version == kVersion; } +bool FeatureSchemaV1::isCompatible(const std::string& version) { + const auto current = parseVersion(kVersion); + const auto provided = parseVersion(version); + + // Both versions must be valid + if (!current.valid || !provided.valid) { + return false; + } + + // Major version must match exactly (breaking changes) + if (provided.major != current.major) { + return false; + } + + // Minor version must be >= current (backward compatible) + if (provided.minor < current.minor) { + return false; + } + + // If minor version is greater, patch doesn't matter (newer compatible version) + if (provided.minor > current.minor) { + return true; + } + + // Minor versions match, so patch must be >= current + return provided.patch >= current.patch; +} size_t FeatureSchemaV1::featureCount() { return names().size(); } diff --git a/tests/unit/AiExtensionTests.cpp b/tests/unit/AiExtensionTests.cpp index 485d11d..3d32738 100644 --- a/tests/unit/AiExtensionTests.cpp +++ b/tests/unit/AiExtensionTests.cpp @@ -161,6 +161,40 @@ TEST_CASE("Feature schema exposes rich feature vector for AI plans", "[ai]") { REQUIRE(automix::ai::FeatureSchemaV1::featureCount() >= 20); } +TEST_CASE("Feature schema version compatibility uses semantic versioning", "[ai]") { + // Exact version match should be compatible + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.0.0")); + + // Patch version updates should be compatible (backward compatible) + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.0.1")); + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.0.2")); + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.0.99")); + + // Minor version updates should be compatible (backward compatible) + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.1.0")); + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.2.0")); + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.99.0")); + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.1.5")); + + // Different major version should be incompatible (breaking changes) + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("0.9.0")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("2.0.0")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("2.1.0")); + + // Invalid or malformed versions should be incompatible + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("invalid")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("1.x.0")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("a.b.c")); + + // Partial versions (missing components) should be handled + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1")); + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.0")); + REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.1")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("0")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("2")); +} + TEST_CASE("Model manager scans demo packs from assets roots", "[ai]") { automix::ai::ModelManager manager("missing_root_for_test"); const auto packs = manager.scan(); From 679756d5b4c25ceeec74aab6d6183c26ae666b49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 03:55:23 +0000 Subject: [PATCH 3/3] fix: require strict semver format (major.minor.patch) and reject partial versions Co-authored-by: soficis <107279009+soficis@users.noreply.github.com> --- src/ai/FeatureSchema.cpp | 6 ++---- tests/unit/AiExtensionTests.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ai/FeatureSchema.cpp b/src/ai/FeatureSchema.cpp index 5f6c6c3..05e0a17 100644 --- a/src/ai/FeatureSchema.cpp +++ b/src/ai/FeatureSchema.cpp @@ -29,8 +29,7 @@ SemanticVersion parseVersion(const std::string& versionStr) { try { // Parse major version if (dotPos == std::string::npos) { - version.major = std::stoi(versionStr); - version.valid = true; + // Partial version with only major - not valid for strict semver return version; } @@ -40,8 +39,7 @@ SemanticVersion parseVersion(const std::string& versionStr) { // Parse minor version dotPos = versionStr.find('.', pos); if (dotPos == std::string::npos) { - version.minor = std::stoi(versionStr.substr(pos)); - version.valid = true; + // Partial version with major.minor only - not valid for strict semver return version; } diff --git a/tests/unit/AiExtensionTests.cpp b/tests/unit/AiExtensionTests.cpp index 3d32738..d0fd135 100644 --- a/tests/unit/AiExtensionTests.cpp +++ b/tests/unit/AiExtensionTests.cpp @@ -187,10 +187,10 @@ TEST_CASE("Feature schema version compatibility uses semantic versioning", "[ai] REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("1.x.0")); REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("a.b.c")); - // Partial versions (missing components) should be handled - REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1")); - REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.0")); - REQUIRE(automix::ai::FeatureSchemaV1::isCompatible("1.1")); + // Partial versions (missing components) should be rejected per strict semver + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("1")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("1.0")); + REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("1.1")); REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("0")); REQUIRE_FALSE(automix::ai::FeatureSchemaV1::isCompatible("2")); }