Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion src/ai/FeatureSchema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,53 @@ double vectorValueOrDefault(const std::vector<double>& 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) {
// Partial version with only major - not valid for strict semver
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) {
// Partial version with major.minor only - not valid for strict semver
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<std::string>& FeatureSchemaV1::names() {
Expand Down Expand Up @@ -83,7 +130,33 @@ const std::vector<std::string>& 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(); }

Expand Down
34 changes: 34 additions & 0 deletions tests/unit/AiExtensionTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 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"));
}

TEST_CASE("Model manager scans demo packs from assets roots", "[ai]") {
automix::ai::ModelManager manager("missing_root_for_test");
const auto packs = manager.scan();
Expand Down