Close all 26 generator readiness gaps (sprints 267-270)

Sprint 268: GR-002/003/006/007/008/016/017/019/021/024 — self-containment,
  graduation thresholds, asm/ir/jvm/elixir lang coverage, semanno format.
Sprint 269: GR-005/011/013/015 — prose capture in RequirementsParser,
  GateEnforcer, ParitySkewAnalyzer, CrossArtifactConsistencyEngine.
Sprint 270: GR-009/010/012/014/018/020 — CrossFileTransactionGate,
  SemanticCompletionGate, ConstrainedProjectionGate, TokenBudgetGate,
  CppConstraintRefactorPolicy, NativeDecompositionDepthGuard.
All 50 new tests passing (steps 1873-1882, 5/5 per step).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-28 22:28:41 -07:00
parent 423273a14b
commit de5bdd358f
25 changed files with 2607 additions and 21 deletions

View File

@@ -0,0 +1,42 @@
#pragma once
// Step 1878: Constrained projection gate.
// Closes GR-012: ProjectionGenerator.generate() has no access to EnvironmentSpec,
// so generators emit code without checking environment constraints. This gate runs
// validateCapabilities + validateEnvAnnotations before generation and blocks on errors.
#include "EnvironmentSpec.h"
#include "ast/ASTNode.h"
#include <string>
#include <vector>
struct ProjectionConstraintResult {
bool blocked = false;
std::vector<CapabilityDiagnostic> diagnostics;
};
class ConstrainedProjectionGate {
public:
// Validate the AST node against the given environment before generation.
// Returns blocked=true when any "error" severity diagnostic is found.
// A null env means no constraints — always passes.
static ProjectionConstraintResult validate(const ASTNode* node,
const EnvironmentSpec* env) {
ProjectionConstraintResult result;
if (!env) return result; // no env = no constraints
auto capDiags = validateCapabilities(node, env);
auto annDiags = validateEnvAnnotations(node, env);
for (const auto& d : capDiags) result.diagnostics.push_back(d);
for (const auto& d : annDiags) result.diagnostics.push_back(d);
for (const auto& d : result.diagnostics) {
if (d.severity == "error") {
result.blocked = true;
break;
}
}
return result;
}
};

View File

@@ -0,0 +1,49 @@
#pragma once
// Step 1881c: C++ constrained refactor policy (GR-018).
// Closes GR-018: enforcement was contract-level but generator capability under
// constraints was weak in the C++ AB path. This policy validates that a C++
// refactor proposal satisfies the required constraint signals before the generator
// is invoked — preventing constraint-violating code from being emitted.
#include <string>
#include <vector>
struct RefactorConstraints {
bool requiresRollbackPlan = false;
bool requiresSecurityReview = false;
bool requiresSloP95 = false;
bool requiresStagedRollout = false;
};
struct CppRefactorPolicyResult {
bool allowed = false;
std::vector<std::string> violations;
};
class CppConstraintRefactorPolicy {
public:
static CppRefactorPolicyResult evaluate(
const RefactorConstraints& required,
bool hasRollbackPlan,
bool hasSecurityReview,
bool hasSloP95,
bool hasStagedRollout)
{
CppRefactorPolicyResult result;
if (required.requiresRollbackPlan && !hasRollbackPlan)
result.violations.push_back("missing_rollback_plan");
if (required.requiresSecurityReview && !hasSecurityReview)
result.violations.push_back("missing_security_review");
if (required.requiresSloP95 && !hasSloP95)
result.violations.push_back("missing_slo_p95");
if (required.requiresStagedRollout && !hasStagedRollout)
result.violations.push_back("missing_staged_rollout");
result.allowed = result.violations.empty();
return result;
}
};

View File

@@ -0,0 +1,63 @@
#pragma once
// Step 1880: Cross-file transaction gate.
// Closes GR-009: long-range edits touch multiple files but DeploymentPromotionGate
// tracked single environments independently. CrossFileTransactionGate makes
// multi-file atomicity explicit: the transaction only promotes if ALL member
// files pass their individual promotion check.
#include "DeploymentPromotionGate.h"
#include <string>
#include <vector>
struct FileEditState {
std::string fileId;
int requiredChecks = 0;
int passedChecks = 0;
int manualApprovals = 0;
int blockingIssues = 0;
};
struct TransactionGateResult {
bool promotable = false;
std::vector<std::string> blockedFiles;
int totalFiles = 0;
int readyFiles = 0;
};
class CrossFileTransactionGate {
public:
static TransactionGateResult evaluate(const std::vector<FileEditState>& files) {
TransactionGateResult result;
result.totalFiles = static_cast<int>(files.size());
if (files.empty()) {
result.promotable = true;
return result;
}
DeploymentPromotionGate gate;
std::string error;
for (const auto& f : files) {
EnvironmentGateState state;
state.environmentId = f.fileId;
state.requiredChecks = f.requiredChecks;
state.passedChecks = f.passedChecks;
state.manualApprovals = f.manualApprovals;
state.blockingIssues = f.blockingIssues;
gate.upsert(state, &error);
}
for (const auto& f : files) {
if (gate.promotable(f.fileId, &error)) {
++result.readyFiles;
} else {
result.blockedFiles.push_back(f.fileId);
}
}
result.promotable = result.blockedFiles.empty();
return result;
}
};

33
editor/src/GateEnforcer.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
// Step 1874: Gate enforcement — block-severity gates halt production.
// GateSeverityPolicy classifies gate severity; GateEnforcer evaluates a list
// of check results and returns a blocked decision when any "block" gate fails.
#include "gates/GateSeverityPolicy.h"
#include <string>
#include <vector>
struct GateCheckResult {
std::string gate;
bool passed;
};
struct GateEnforcementResult {
bool blocked = false;
std::vector<std::string> blockingGates;
};
class GateEnforcer {
public:
static GateEnforcementResult evaluate(const std::vector<GateCheckResult>& checks) {
GateEnforcementResult result;
for (const auto& check : checks) {
if (!check.passed && GateSeverityPolicy::severityFor(check.gate) == "block") {
result.blocked = true;
result.blockingGates.push_back(check.gate);
}
}
return result;
}
};

View File

@@ -0,0 +1,37 @@
#pragma once
// Step 1881b: Native decomposition depth guard (GR-020).
// Closes GR-020: native decomposition task depth was stuck at 2 and retry
// did not improve it. This guard enforces a minimum task count per decomposition
// attempt and rejects shallow decompositions before they can mask weak generation.
#include <string>
#include <vector>
struct DecompositionGuardResult {
bool accepted = false;
std::string reason;
int taskCount = 0;
int minRequired = 0;
};
class NativeDecompositionDepthGuard {
public:
static constexpr int kMinTaskCount = 3; // minimum tasks for a valid decomposition
static DecompositionGuardResult evaluate(const std::vector<std::string>& tasks,
int minRequired = kMinTaskCount) {
DecompositionGuardResult result;
result.taskCount = static_cast<int>(tasks.size());
result.minRequired = minRequired;
if (result.taskCount < minRequired) {
result.accepted = false;
result.reason = "decomposition_too_shallow";
return result;
}
result.accepted = true;
result.reason = "depth_sufficient";
return result;
}
};

View File

@@ -0,0 +1,61 @@
#pragma once
// Step 1876: Parity skew analyzer — detect cross-language readiness asymmetry.
// Closes GR-013: CrossLanguageConsistencyGate evaluates per-request but has no
// batch analysis or skew tracking. This adds both.
#include "CrossLanguageConsistencyGate.h"
#include <string>
#include <vector>
struct LanguageReadiness {
std::string language;
bool ready; // supported && allowed for the requested construct/operation
};
struct ParitySkewResult {
int readyCount = 0;
int totalCount = 0;
float skew = 0.0f; // 0 = all ready, 1 = none ready
std::vector<std::string> notReadyLanguages;
bool belowThreshold = false; // true when skew > skewThreshold
};
class ParitySkewAnalyzer {
public:
// Analyze a pre-built readiness list.
static ParitySkewResult analyze(const std::vector<LanguageReadiness>& languages,
float skewThreshold = 0.5f) {
ParitySkewResult result;
result.totalCount = static_cast<int>(languages.size());
for (const auto& lang : languages) {
if (lang.ready) {
++result.readyCount;
} else {
result.notReadyLanguages.push_back(lang.language);
}
}
result.skew = (result.totalCount == 0)
? 0.0f
: 1.0f - static_cast<float>(result.readyCount) / result.totalCount;
result.belowThreshold = result.skew > skewThreshold;
return result;
}
// Batch-evaluate a construct+operation across a set of languages via the gate.
static ParitySkewResult evaluateConstruct(const std::vector<std::string>& languages,
const std::string& constructKind,
const std::string& operation,
float skewThreshold = 0.5f) {
std::vector<LanguageReadiness> readiness;
for (const auto& lang : languages) {
CrossLanguageRequest req;
req.language = lang;
req.constructKind = constructKind;
req.operation = operation;
auto r = CrossLanguageConsistencyGate::evaluate(req);
readiness.push_back({lang, r.supported && r.allowed});
}
return analyze(readiness, skewThreshold);
}
};

View File

@@ -0,0 +1,185 @@
#pragma once
#include "IntakeTextUtil.h"
#include <algorithm>
#include <cctype>
#include <sstream>
#include <string>
#include <vector>
struct ParsedRequirement {
std::string text;
std::string sourceText;
int sourceLine = 0;
std::string sectionAnchor;
std::string sectionTitle;
bool isField = false;
std::string fieldName;
std::string fieldType;
};
class RequirementsParser {
public:
static std::vector<ParsedRequirement> parse(const std::string& markdown) {
std::vector<ParsedRequirement> out;
if (markdown.empty()) return out;
std::istringstream stream(markdown);
std::string line;
std::string sectionTitle;
std::string sectionAnchor;
int lineNo = 0;
while (std::getline(stream, line)) {
++lineNo;
std::string trimmed = trim(line);
if (isHeading(trimmed)) {
sectionTitle = trimHeading(trimmed);
sectionAnchor = anchorize(sectionTitle);
continue;
}
if (!isRequirementHeading(sectionAnchor)) continue;
if (isBullet(trimmed)) {
std::string bulletText = trim(trimmed.substr(2));
if (bulletText.empty()) continue;
if (tryParseFieldList(bulletText, lineNo, sectionTitle, sectionAnchor, &out)) continue;
ParsedRequirement req;
req.text = normalizeText(bulletText);
req.sourceText = bulletText;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
out.push_back(req);
} else if (isProseLine(trimmed)) {
ParsedRequirement req;
req.text = normalizeText(trimmed);
req.sourceText = trimmed;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
out.push_back(req);
}
}
return out;
}
static std::string anchorize(const std::string& heading) {
std::string out;
bool prevDash = false;
for (unsigned char ch : heading) {
if (std::isalnum(ch)) {
out.push_back(static_cast<char>(std::tolower(ch)));
prevDash = false;
} else if (!out.empty() && !prevDash) {
out.push_back('-');
prevDash = true;
}
}
while (!out.empty() && out.back() == '-') out.pop_back();
return out;
}
static bool isRequirementHeading(const std::string& anchor) {
return anchor == "requirements" ||
anchor == "features" ||
anchor == "behavior" ||
anchor == "behaviour" ||
anchor == "interface" ||
anchor == "api";
}
private:
static std::string trim(const std::string& value) {
std::size_t begin = 0;
while (begin < value.size() && std::isspace(static_cast<unsigned char>(value[begin]))) ++begin;
std::size_t end = value.size();
while (end > begin && std::isspace(static_cast<unsigned char>(value[end - 1]))) --end;
return value.substr(begin, end - begin);
}
static std::string normalizeText(const std::string& text) {
return intakeCompactLowerAlnumSpaces(text);
}
static bool isHeading(const std::string& line) {
if (line.size() < 2) return false;
if (line[0] != '#') return false;
std::size_t hashes = 0;
while (hashes < line.size() && line[hashes] == '#') ++hashes;
return hashes > 0 && hashes < line.size() && line[hashes] == ' ';
}
static std::string trimHeading(const std::string& headingLine) {
std::size_t i = 0;
while (i < headingLine.size() && headingLine[i] == '#') ++i;
if (i < headingLine.size() && headingLine[i] == ' ') ++i;
return trim(headingLine.substr(i));
}
static bool isBullet(const std::string& line) {
if (line.size() < 3) return false;
return (line[0] == '-' || line[0] == '*') && line[1] == ' ';
}
static bool isProseLine(const std::string& trimmed) {
if (trimmed.size() < 10) return false;
if (isHeading(trimmed)) return false;
if (trimmed[0] == '`' || trimmed[0] == '|' || trimmed[0] == '>') return false;
return true;
}
static std::string maybeFieldType(const std::string& token) {
std::size_t l = token.find('(');
std::size_t r = token.rfind(')');
if (l == std::string::npos || r == std::string::npos || r <= l) return "";
return trim(token.substr(l + 1, r - l - 1));
}
static std::string fieldNameFromToken(const std::string& token) {
std::string work = trim(token);
std::size_t l = work.find('(');
if (l != std::string::npos) work = trim(work.substr(0, l));
return work;
}
static bool tryParseFieldList(const std::string& bulletText,
int sourceLine,
const std::string& sectionTitle,
const std::string& sectionAnchor,
std::vector<ParsedRequirement>* out) {
std::size_t colon = bulletText.find(':');
if (colon == std::string::npos) return false;
std::string prefix = trim(bulletText.substr(0, colon));
std::string lowerPrefix = intakeToLower(prefix);
if (lowerPrefix.find(" has") == std::string::npos && lowerPrefix != "has") return false;
std::string rhs = trim(bulletText.substr(colon + 1));
if (rhs.find(',') == std::string::npos) return false;
std::stringstream ss(rhs);
std::string token;
bool emitted = false;
while (std::getline(ss, token, ',')) {
std::string name = fieldNameFromToken(token);
if (name.empty()) continue;
ParsedRequirement req;
req.isField = true;
req.fieldName = name;
req.fieldType = maybeFieldType(token);
req.sourceText = trim(token);
req.text = normalizeText(prefix + " " + name + (req.fieldType.empty() ? "" : " " + req.fieldType));
req.sourceLine = sourceLine;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
out->push_back(req);
emitted = true;
}
return emitted;
}
};

View File

@@ -10,6 +10,7 @@
struct ScoredTaskitem {
std::string taskId;
int score = 0;
int executionSpecificityScore = 0;
bool selfContained = false;
std::vector<std::string> issues;
};
@@ -21,6 +22,11 @@ struct TaskitemInput {
std::vector<std::string> reasons;
int confidence = 0;
std::vector<std::string> dependencyTaskIds;
int executionSpecificityScore = 0;
bool strictExecutionContract = false;
bool capabilityCompileFailed = false;
bool capabilityTestsFailed = false;
bool capabilityToolchainMissing = false;
};
class SelfContainmentScorer {
@@ -54,6 +60,9 @@ public:
if (item.reasons.empty()) {
scoreValue -= 20;
out.issues.push_back("reasons missing or empty");
} else if (item.reasons.size() == 1) {
scoreValue -= 25;
out.issues.push_back("reasons has fewer than 2 entries");
} else if (item.reasons.size() < 3) {
scoreValue -= 10;
out.issues.push_back("reasons has fewer than 3 entries");
@@ -74,9 +83,25 @@ public:
out.issues.push_back("dependencyTaskIds not empty");
}
if (item.strictExecutionContract) {
if (item.executionSpecificityScore < 60) {
scoreValue -= 25;
out.issues.push_back("executionSpecificityScore below strict threshold");
}
if (item.executionSpecificityScore == 0) {
scoreValue -= 10;
out.issues.push_back("execution contract metadata missing");
}
}
if (item.capabilityCompileFailed || item.capabilityTestsFailed || item.capabilityToolchainMissing) {
scoreValue -= 25;
out.issues.push_back("capability_signals indicate generator/runtime failure");
}
out.score = std::clamp(scoreValue, 0, 100);
out.executionSpecificityScore = std::clamp(item.executionSpecificityScore, 0, 100);
out.selfContained = out.score >= 80;
return out;
}
};

View File

@@ -0,0 +1,50 @@
#pragma once
// Step 1879: Semantic completion gate.
// Closes GR-010: ImprovementPromotionGate had a threshold of 0.02 (nearly anything passes)
// and no depth tracking. SemanticCompletionGate enforces a meaningful semantic score
// threshold and caps recursive completion depth to prevent unbounded iteration.
#include <string>
#include <vector>
struct SemanticCompletionResult {
bool passed = false;
std::string reason;
int depth = 0;
float score = 0.0f;
};
class SemanticCompletionGate {
public:
static constexpr float kMinScore = 0.70f;
static constexpr int kMaxDepth = 5;
// Evaluate whether a completion attempt should be promoted or retried.
// score: semantic equivalence/coverage score in [0,1]
// depth: current recursive completion depth (starts at 0)
// humanApproved: whether human review has been applied
static SemanticCompletionResult evaluate(float score,
int depth,
bool humanApproved = false) {
SemanticCompletionResult result;
result.score = score;
result.depth = depth;
if (depth > kMaxDepth) {
result.passed = false;
result.reason = "max_depth_exceeded";
return result;
}
if (score < kMinScore) {
result.passed = false;
result.reason = "score_below_semantic_threshold";
return result;
}
// At or above threshold: pass if human-approved or on a stable path
result.passed = humanApproved || score >= 0.90f;
result.reason = result.passed ? "accepted" : "human_approval_required";
return result;
}
};

View File

@@ -0,0 +1,44 @@
#pragma once
// Step 1881a: Token budget gate (GR-014).
// Closes GR-014: low-yield (token-bloated) runs had no hard enforcement.
// TokenBudgetGate uses ContextWindowOptimizer to estimate tokens and blocks
// when a generation output would exceed the allowed budget.
#include "ContextWindowOptimizer.h"
#include <string>
struct TokenBudgetResult {
bool blocked = false;
int estimatedTokens = 0;
int budgetTokens = 0;
float yieldRatio = 0.0f; // output useful content / total tokens
};
class TokenBudgetGate {
public:
static constexpr float kMinYieldRatio = 0.30f; // at least 30% of tokens must be useful
// Block if the output text exceeds the budget or yield is too low.
static TokenBudgetResult evaluate(const std::string& outputText,
int contextWindowTokens,
int usefulContentTokens = -1) {
TokenBudgetResult result;
result.estimatedTokens = ContextWindowOptimizer::estimateTokens(outputText);
result.budgetTokens = ContextWindowOptimizer::budgetForContextWindow(contextWindowTokens);
if (result.estimatedTokens > result.budgetTokens) {
result.blocked = true;
return result;
}
// Yield check: if caller provides useful content token count
if (usefulContentTokens >= 0 && result.estimatedTokens > 0) {
result.yieldRatio = static_cast<float>(usefulContentTokens) / result.estimatedTokens;
if (result.yieldRatio < kMinYieldRatio) {
result.blocked = true;
}
}
return result;
}
};

View File

@@ -0,0 +1,86 @@
#pragma once
// Step 1875: Cross-artifact consistency engine.
// Checks that spec goal keywords appear in generated taskitem intents.
// Closes GR-015: previously CrossArtifactConsistencyChecker was a data stub;
// this provides the actual evaluation logic.
#include <algorithm>
#include <cctype>
#include <string>
#include <vector>
struct ConsistencyResult {
int inconsistencyCount = 0;
bool consistent = false;
std::vector<std::string> driftedGoals;
};
class CrossArtifactConsistencyEngine {
public:
// Check that each spec goal has at least one meaningful keyword present
// in at least one taskitem intent. Goals with no coverage are drift.
static ConsistencyResult check(const std::vector<std::string>& specGoals,
const std::vector<std::string>& taskitemIntents) {
ConsistencyResult result;
for (const auto& goal : specGoals) {
if (!anyIntentCoversGoal(goal, taskitemIntents)) {
++result.inconsistencyCount;
result.driftedGoals.push_back(goal);
}
}
result.consistent = result.inconsistencyCount == 0;
return result;
}
private:
static std::vector<std::string> tokenize(const std::string& text) {
std::vector<std::string> tokens;
std::string tok;
for (unsigned char c : text) {
if (std::isalnum(c)) {
tok += static_cast<char>(std::tolower(c));
} else if (!tok.empty()) {
tokens.push_back(tok);
tok.clear();
}
}
if (!tok.empty()) tokens.push_back(tok);
return tokens;
}
static bool isStopWord(const std::string& tok) {
static const std::vector<std::string> stops = {
"the", "and", "must", "should", "will", "that", "with", "for", "are", "has"
};
return std::find(stops.begin(), stops.end(), tok) != stops.end();
}
static std::string toLower(const std::string& s) {
std::string out = s;
for (auto& c : out) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return out;
}
static bool anyIntentCoversGoal(const std::string& goal,
const std::vector<std::string>& intents) {
auto goalTokens = tokenize(goal);
goalTokens.erase(
std::remove_if(goalTokens.begin(), goalTokens.end(), isStopWord),
goalTokens.end());
// Only consider tokens of meaningful length (avoid short noise words)
std::vector<std::string> meaningful;
for (const auto& tok : goalTokens) {
if (tok.size() >= 4) meaningful.push_back(tok);
}
if (meaningful.empty()) return true; // no meaningful tokens → not flagged
// A goal is covered if at least one meaningful token appears in any intent
for (const auto& tok : meaningful) {
for (const auto& intent : intents) {
if (toLower(intent).find(tok) != std::string::npos) return true;
}
}
return false;
}
};