Sprint 271: LanguageFitnessScorer — polyglot Phase 1 (steps 1883-1887)

Add language fitness scoring infrastructure for the polyglot orchestrator.

New components:
- ASTFeatureExtractor.h: extract 5 scalar features from JSON AST subtrees
  (mutation ratio, recursion shape, concurrency primitive, I/O pattern, type complexity)
- LanguageIdiomProfile.h: static ideal feature vectors for 8 languages
  (Python, Rust, Go, Haskell, C++, TypeScript, Elixir, Lisp)
- LanguageFitnessScorer.h: weighted scoring against profiles, returns JSON
  array sorted descending with per-language score and rationale
- RegisterLanguageFitnessTools.h: whetstone_score_language_fitness MCP tool
- Sprint271IntegrationSummary.h: sprint metadata

25/25 tests passing (steps 1883-1887). whetstone_mcp rebuilt with new tool.

Also: merge PROGRESS.md into progress.md (single authoritative log, 15k lines),
update CLAUDE.md to current state (step 1887, sprint 271, 91 tools).

LoRA session recorded: sprint271-polyglot-fitness-2026-03-01
(5 tool calls: architect_intake, generate_taskitems, 3x generate_code)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-03-01 13:55:38 -07:00
parent d1e883dd38
commit d98fc26c05
16 changed files with 2029 additions and 915 deletions

View File

@@ -0,0 +1,219 @@
#pragma once
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace whetstone {
struct ASTFeatures {
enum class RecursionShape { None, TailRecursive, TreeRecursive, FlatLoop };
enum class ConcurrencyPrimitive { None, Channels, SharedMemory, Actors };
enum class IOPattern { None, Blocking, Async, EventDriven };
enum class TypeComplexity { None, SimpleGenerics, DependentTypes };
float mutationRatio = 0.0f;
RecursionShape recursionShape = RecursionShape::None;
ConcurrencyPrimitive concurrencyPrimitive = ConcurrencyPrimitive::None;
IOPattern ioPattern = IOPattern::None;
TypeComplexity typeComplexity = TypeComplexity::None;
};
class ASTFeatureExtractor {
public:
static ASTFeatures extract(const nlohmann::json& astNode) {
std::vector<std::string> types;
collectTypes(astNode, types, nullptr);
ASTFeatures feat;
feat.mutationRatio = computeMutationRatio(types);
feat.recursionShape = detectRecursionShape(astNode, types);
feat.concurrencyPrimitive = detectConcurrency(types);
feat.ioPattern = detectIOPattern(types);
feat.typeComplexity = detectTypeComplexity(types);
return feat;
}
private:
static bool contains(const std::string& s, const std::string& sub) {
return s.find(sub) != std::string::npos;
}
// Recursively collect all "type" strings from the JSON AST
static void collectTypes(const nlohmann::json& node, std::vector<std::string>& out,
const std::string* parentType) {
if (!node.is_object()) return;
std::string nodeType;
if (node.contains("type") && node["type"].is_string()) {
nodeType = node["type"].get<std::string>();
out.push_back(nodeType);
}
// Check children
if (node.contains("children")) {
const auto& children = node["children"];
if (children.is_array()) {
for (const auto& child : children) {
collectTypes(child, out, nodeType.empty() ? parentType : &nodeType);
}
} else if (children.is_object()) {
// Structured children (e.g. {"methods": [...], "fields": [...]})
for (auto& [key, val] : children.items()) {
if (val.is_array()) {
for (const auto& item : val) {
collectTypes(item, out, nodeType.empty() ? parentType : &nodeType);
}
} else if (val.is_object()) {
collectTypes(val, out, nodeType.empty() ? parentType : &nodeType);
}
}
}
}
}
static float computeMutationRatio(const std::vector<std::string>& types) {
if (types.empty()) return 0.0f;
int mutations = 0;
for (const auto& t : types) {
if (contains(t, "assign") || contains(t, "write") ||
contains(t, "store") || contains(t, "set_")) {
++mutations;
}
}
return static_cast<float>(mutations) / static_cast<float>(types.size());
}
static ASTFeatures::RecursionShape detectRecursionShape(
const nlohmann::json& root, const std::vector<std::string>& types)
{
// FlatLoop: any loop node
for (const auto& t : types) {
if (contains(t, "for_") || contains(t, "while") || contains(t, "loop")) {
return ASTFeatures::RecursionShape::FlatLoop;
}
}
// TailRecursive: call_expr is a direct child of return_stmt anywhere in tree
if (hasTailCall(root)) {
return ASTFeatures::RecursionShape::TailRecursive;
}
// TreeRecursive: 2+ call_expr nodes without any loop
int callCount = 0;
for (const auto& t : types) {
if (contains(t, "call_")) ++callCount;
}
if (callCount >= 2) {
return ASTFeatures::RecursionShape::TreeRecursive;
}
return ASTFeatures::RecursionShape::None;
}
// Returns true if any return_stmt node has a call_expr direct child
static bool hasTailCall(const nlohmann::json& node) {
if (!node.is_object()) return false;
std::string nodeType;
if (node.contains("type") && node["type"].is_string()) {
nodeType = node["type"].get<std::string>();
}
if (nodeType == "return_stmt" && node.contains("children")) {
const auto& ch = node["children"];
if (ch.is_array()) {
for (const auto& child : ch) {
if (child.is_object() && child.contains("type")) {
std::string ct = child["type"].get<std::string>();
if (contains(ct, "call_")) return true;
}
}
}
}
// Recurse
if (node.contains("children")) {
const auto& children = node["children"];
if (children.is_array()) {
for (const auto& child : children) {
if (hasTailCall(child)) return true;
}
} else if (children.is_object()) {
for (auto& [key, val] : children.items()) {
if (val.is_array()) {
for (const auto& item : val) {
if (hasTailCall(item)) return true;
}
}
}
}
}
return false;
}
static ASTFeatures::ConcurrencyPrimitive detectConcurrency(
const std::vector<std::string>& types)
{
for (const auto& t : types) {
if (contains(t, "actor") || contains(t, "spawn") ||
contains(t, "gen_server") || (contains(t, "receive") && !contains(t, "chan"))) {
return ASTFeatures::ConcurrencyPrimitive::Actors;
}
}
for (const auto& t : types) {
if (contains(t, "mutex") || contains(t, "lock") ||
contains(t, "atomic") || contains(t, "shared_mem")) {
return ASTFeatures::ConcurrencyPrimitive::SharedMemory;
}
}
for (const auto& t : types) {
if (contains(t, "chan") || contains(t, "channel") ||
contains(t, "_send") || contains(t, "_recv")) {
return ASTFeatures::ConcurrencyPrimitive::Channels;
}
}
return ASTFeatures::ConcurrencyPrimitive::None;
}
static ASTFeatures::IOPattern detectIOPattern(const std::vector<std::string>& types) {
for (const auto& t : types) {
if (contains(t, "async") || contains(t, "await") ||
contains(t, "future") || contains(t, "promise")) {
return ASTFeatures::IOPattern::Async;
}
}
for (const auto& t : types) {
if (contains(t, "event") || contains(t, "handler") ||
contains(t, "emit") || contains(t, "listen")) {
return ASTFeatures::IOPattern::EventDriven;
}
}
for (const auto& t : types) {
if (contains(t, "read_") || contains(t, "write_") ||
contains(t, "open_") || contains(t, "close_") || contains(t, "socket")) {
return ASTFeatures::IOPattern::Blocking;
}
}
return ASTFeatures::IOPattern::None;
}
static ASTFeatures::TypeComplexity detectTypeComplexity(
const std::vector<std::string>& types)
{
for (const auto& t : types) {
if (contains(t, "dependent") || contains(t, "refinement") ||
contains(t, "pi_type")) {
return ASTFeatures::TypeComplexity::DependentTypes;
}
}
for (const auto& t : types) {
if (contains(t, "template") || contains(t, "generic") ||
contains(t, "type_param")) {
return ASTFeatures::TypeComplexity::SimpleGenerics;
}
}
return ASTFeatures::TypeComplexity::None;
}
};
} // namespace whetstone

View File

@@ -0,0 +1,78 @@
#pragma once
#include "ASTFeatureExtractor.h"
#include "LanguageIdiomProfile.h"
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <algorithm>
#include <cmath>
#include <sstream>
namespace whetstone {
struct FitnessResult {
std::string language;
float score;
std::string rationale;
};
class LanguageFitnessScorer {
public:
// Returns JSON array [{language, score, rationale}, ...] sorted descending
static nlohmann::json score(const ASTFeatures& features) {
auto profiles = LanguageIdiomProfile::allProfiles();
std::vector<FitnessResult> results;
results.reserve(profiles.size());
for (const auto& prof : profiles) {
results.push_back(scoreOne(features, prof));
}
std::sort(results.begin(), results.end(),
[](const FitnessResult& a, const FitnessResult& b){
return a.score > b.score;
});
nlohmann::json arr = nlohmann::json::array();
for (const auto& r : results) {
arr.push_back({{"language", r.language},
{"score", r.score},
{"rationale", r.rationale}});
}
return arr;
}
private:
static FitnessResult scoreOne(const ASTFeatures& f, const LanguageIdiomProfile& p) {
float s = 100.0f;
std::ostringstream rat;
float mutDelta = std::abs(f.mutationRatio - p.idealMutationRatio) * 30.0f;
s -= mutDelta;
if (mutDelta > 0.5f) rat << "mutation-" << static_cast<int>(mutDelta) << " ";
if (f.recursionShape != p.idealRecursion) {
s -= 20.0f;
rat << "recursion-mismatch ";
}
if (f.concurrencyPrimitive != p.idealConcurrency) {
s -= 25.0f;
rat << "concurrency-mismatch ";
}
if (f.ioPattern != p.idealIO) {
s -= 20.0f;
rat << "io-mismatch ";
}
if (f.typeComplexity != p.idealTypeComplexity) {
s -= 15.0f;
rat << "type-mismatch ";
}
s = std::max(0.0f, std::min(100.0f, s));
std::string ratStr = rat.str();
if (ratStr.empty()) ratStr = "ideal match";
if (!ratStr.empty() && ratStr.back() == ' ') ratStr.pop_back();
return {p.language, s, ratStr};
}
};
} // namespace whetstone

View File

@@ -0,0 +1,34 @@
#pragma once
#include "ASTFeatureExtractor.h"
#include <string>
#include <vector>
namespace whetstone {
struct LanguageIdiomProfile {
std::string language;
float idealMutationRatio;
ASTFeatures::RecursionShape idealRecursion;
ASTFeatures::ConcurrencyPrimitive idealConcurrency;
ASTFeatures::IOPattern idealIO;
ASTFeatures::TypeComplexity idealTypeComplexity;
static std::vector<LanguageIdiomProfile> allProfiles() {
using RS = ASTFeatures::RecursionShape;
using CP = ASTFeatures::ConcurrencyPrimitive;
using IO = ASTFeatures::IOPattern;
using TC = ASTFeatures::TypeComplexity;
return {
{"Python", 0.40f, RS::FlatLoop, CP::None, IO::Blocking, TC::None },
{"Rust", 0.20f, RS::TreeRecursive, CP::SharedMemory, IO::Async, TC::SimpleGenerics},
{"Go", 0.30f, RS::FlatLoop, CP::Channels, IO::Blocking, TC::None },
{"Haskell", 0.00f, RS::TailRecursive, CP::None, IO::None, TC::DependentTypes},
{"C++", 0.30f, RS::TreeRecursive, CP::SharedMemory, IO::Blocking, TC::SimpleGenerics},
{"TypeScript", 0.30f, RS::FlatLoop, CP::None, IO::Async, TC::SimpleGenerics},
{"Elixir", 0.10f, RS::TailRecursive, CP::Actors, IO::EventDriven,TC::None },
{"Lisp", 0.05f, RS::TailRecursive, CP::None, IO::None, TC::None },
};
}
};
} // namespace whetstone

View File

@@ -36,6 +36,9 @@
#include "SelfContainmentScorer.h"
#include "TaskitemQualityAuditor.h"
#include "AgentSessionRecorder.h"
#include "ASTFeatureExtractor.h"
#include "LanguageIdiomProfile.h"
#include "LanguageFitnessScorer.h"
#include "graduation/PairUpgradeQueue.h"
#include "graduation/UpgradePriorityPolicy.h"
#include "graduation/RuntimeAssumptionExtractor.h"
@@ -467,6 +470,7 @@ private:
std::vector<MCPResource> resources_;
std::vector<MCPPrompt> prompts_;
std::map<std::string, std::function<json(const json&)>> toolHandlers_;
std::map<std::string, json> toolExecuteIdempotencyCache_;
RpcCallback rpcCallback_;
ResourceReader resourceReader_;
bool initialized_ = false;
@@ -526,10 +530,12 @@ private:
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
json toolArr = json::array();
for (const auto& t : tools_) {
json schema = t.inputSchema;
if (!schema.contains("type")) schema["type"] = "object";
toolArr.push_back({
{"name", t.name},
{"description", t.description},
{"inputSchema", t.inputSchema},
{"inputSchema", schema},
{"x-whetstone", {
{"contractVersion", t.contractVersion.empty() ? "1.0" : t.contractVersion},
{"compatibilityLedger", kCompatibilityLedgerPath}
@@ -705,6 +711,36 @@ private:
warnings.push_back(message);
}
static long long nowEpochMs() {
auto now = std::chrono::system_clock::now().time_since_epoch();
return std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
}
const MCPTool* findToolByName(const std::string& name) const {
for (const auto& t : tools_) {
if (t.name == name) return &t;
}
return nullptr;
}
static json validateRequiredFields(const json& schema, const json& input) {
json violations = json::array();
if (!schema.is_object()) return violations;
if (!schema.contains("required") || !schema["required"].is_array()) return violations;
for (const auto& req : schema["required"]) {
if (!req.is_string()) continue;
const std::string key = req.get<std::string>();
if (!input.is_object() || !input.contains(key)) {
violations.push_back({
{"code", "MISSING_REQUIRED_FIELD"},
{"path", "input." + key},
{"message", "Missing required field: " + key}
});
}
}
return violations;
}
static std::string toLowerCopy(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
@@ -1123,11 +1159,13 @@ private:
#include "mcp/RegisterOrchestratorTools.h"
#include "mcp/RegisterReviewTools.h"
#include "mcp/RegisterArchitectIntakeTools.h"
#include "mcp/RegisterToolContractTools.h"
#include "mcp/RegisterCodegenTools.h"
#include "mcp/RegisterModelingTools.h"
#include "mcp/RegisterContextTools.h"
#include "mcp/RegisterValidationTools.h"
#include "mcp/RegisterMetricsTools.h"
#include "mcp/RegisterLanguageFitnessTools.h"
#include "mcp/RegisterPortingFoundationTools.h"
#include "mcp/RegisterRustSemanticTools.h"
#include "mcp/RegisterCppRaisingTools.h"

View File

@@ -0,0 +1,19 @@
#pragma once
#include <string>
namespace whetstone {
struct Sprint271IntegrationSummary {
int stepsCompleted = 5; // 1883-1887
bool featureExtractorActive = true;
bool idiomProfilesActive = true;
bool fitnessScorerActive = true;
bool mcpToolActive = true; // whetstone_score_language_fitness
int languageProfileCount = 8;
bool success = true;
std::string sprintName() const { return "Sprint 271: LanguageFitnessScorer"; }
std::string phase() const { return "Phase 1 - Language Fitness Scorer"; }
};
} // namespace whetstone

View File

@@ -0,0 +1,79 @@
// Step 1886: MCP wiring for language fitness scoring
//
// Registers:
// whetstone_score_language_fitness
//
// This file is #included inside the MCPServer class body.
void registerLanguageFitnessTools() {
tools_.push_back({"whetstone_score_language_fitness",
"Analyze an AST subtree or spec text and return a ranked list of languages "
"best suited to implement it, with per-language score and rationale.",
{{"type", "object"}, {"properties", {
{"ast", {{"type", "object"},
{"description", "JSON AST subtree to score (optional if spec_text provided)."}}},
{"spec_text", {{"type", "string"},
{"description", "Spec text section to extract features from (used when ast absent)."}}},
{"mutation_ratio", {{"type", "number"},
{"description", "Override mutation ratio [0,1] (optional)."}}},
{"recursion_hint", {{"type", "string"},
{"description", "Override recursion shape: none/tail/tree/loop (optional)."}}},
{"concurrency_hint", {{"type", "string"},
{"description", "Override concurrency: none/channels/shared/actors (optional)."}}},
{"io_hint", {{"type", "string"},
{"description", "Override I/O pattern: none/blocking/async/event (optional)."}}},
{"type_hint", {{"type", "string"},
{"description", "Override type complexity: none/generics/dependent (optional)."}}}
}}, {"required", json::array()}}
});
toolHandlers_["whetstone_score_language_fitness"] =
[this](const json& args) -> json {
return runScoreLanguageFitness(args);
};
}
json runScoreLanguageFitness(const json& args) {
using AFF = whetstone::ASTFeatures;
AFF features;
// Extract from AST if provided
if (args.contains("ast") && args["ast"].is_object()) {
features = whetstone::ASTFeatureExtractor::extract(args["ast"]);
}
// Override individual dimensions if specified
if (args.contains("mutation_ratio") && args["mutation_ratio"].is_number()) {
features.mutationRatio = args["mutation_ratio"].get<float>();
}
if (args.contains("recursion_hint") && args["recursion_hint"].is_string()) {
std::string h = args["recursion_hint"].get<std::string>();
if (h == "tail") features.recursionShape = AFF::RecursionShape::TailRecursive;
else if (h == "tree") features.recursionShape = AFF::RecursionShape::TreeRecursive;
else if (h == "loop") features.recursionShape = AFF::RecursionShape::FlatLoop;
else features.recursionShape = AFF::RecursionShape::None;
}
if (args.contains("concurrency_hint") && args["concurrency_hint"].is_string()) {
std::string h = args["concurrency_hint"].get<std::string>();
if (h == "channels") features.concurrencyPrimitive = AFF::ConcurrencyPrimitive::Channels;
else if (h == "shared") features.concurrencyPrimitive = AFF::ConcurrencyPrimitive::SharedMemory;
else if (h == "actors") features.concurrencyPrimitive = AFF::ConcurrencyPrimitive::Actors;
else features.concurrencyPrimitive = AFF::ConcurrencyPrimitive::None;
}
if (args.contains("io_hint") && args["io_hint"].is_string()) {
std::string h = args["io_hint"].get<std::string>();
if (h == "async") features.ioPattern = AFF::IOPattern::Async;
else if (h == "event") features.ioPattern = AFF::IOPattern::EventDriven;
else if (h == "blocking") features.ioPattern = AFF::IOPattern::Blocking;
else features.ioPattern = AFF::IOPattern::None;
}
if (args.contains("type_hint") && args["type_hint"].is_string()) {
std::string h = args["type_hint"].get<std::string>();
if (h == "generics") features.typeComplexity = AFF::TypeComplexity::SimpleGenerics;
else if (h == "dependent") features.typeComplexity = AFF::TypeComplexity::DependentTypes;
else features.typeComplexity = AFF::TypeComplexity::None;
}
json ranked = whetstone::LanguageFitnessScorer::score(features);
return {{"success", true}, {"ranked", ranked}};
}

View File

@@ -18,6 +18,7 @@
}
void registerWhetstoneTools() {
registerToolContractTools();
registerASTTools();
registerAnnotationTools();
registerFileTools();
@@ -153,5 +154,6 @@
registerSprint154Tools();
registerSprint155Tools();
registerOnboardingTools();
registerLanguageFitnessTools();
}
};