fix(gsd): delete orphaned complexity.ts (#1005)
* fix(gsd): delete orphaned complexity.ts (superseded by complexity-classifier.ts) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): update complexity-routing tests for complexity-classifier.ts The test file imported from the deleted complexity.ts. Removed tests for the defunct classifyTaskComplexity function and updated all source reads to reference complexity-classifier.ts with its actual exports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13f9d5585d
commit
47cff561f0
2 changed files with 29 additions and 437 deletions
|
|
@ -1,224 +0,0 @@
|
|||
/**
|
||||
* GSD Task Complexity Classification
|
||||
*
|
||||
* Classifies task plans and unit types by complexity to enable model routing.
|
||||
* Pure heuristics + adaptive learning — no LLM calls, sub-millisecond.
|
||||
*
|
||||
* Combined approach:
|
||||
* - Task plan analysis (step count, file count, description length, signal words)
|
||||
* - Unit type defaults (complete-slice → light, replan → heavy, etc.)
|
||||
* - Budget pressure thresholds (50/75/90% graduated downgrade)
|
||||
* - Adaptive learning via routing-history (optional)
|
||||
*
|
||||
* Classification output uses our TokenProfile-aligned TaskComplexity type
|
||||
* for the simple classifier, and ComplexityTier for the full unit classifier.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { ComplexityTier, ClassificationResult, TaskMetadata } from "./types.js";
|
||||
|
||||
// Re-export for convenience
|
||||
export type { ComplexityTier, ClassificationResult, TaskMetadata };
|
||||
|
||||
// ─── Simple Task Complexity (for task plan analysis) ──────────────────────
|
||||
|
||||
export type TaskComplexity = "simple" | "standard" | "complex";
|
||||
|
||||
/** Words that signal non-trivial work requiring full reasoning capacity */
|
||||
const COMPLEXITY_SIGNALS = [
|
||||
"research", "investigate", "refactor", "migrate", "integrate",
|
||||
"complex", "architect", "redesign", "security", "performance",
|
||||
"concurrent", "parallel", "distributed", "backward.?compat",
|
||||
"migration", "architecture", "concurrency", "compatibility",
|
||||
];
|
||||
const COMPLEXITY_PATTERN = new RegExp(COMPLEXITY_SIGNALS.join("|"), "i");
|
||||
|
||||
/**
|
||||
* Classify a task plan by its structural complexity.
|
||||
* Used by dispatch to select execution_simple vs execution model.
|
||||
*/
|
||||
export function classifyTaskComplexity(planContent: string): TaskComplexity {
|
||||
if (!planContent || planContent.trim().length === 0) return "standard";
|
||||
|
||||
const stepsMatch = planContent.match(/##\s*Steps\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
|
||||
const stepsSection = stepsMatch?.[1] ?? "";
|
||||
const stepCount = (stepsSection.match(/^\s*\d+\.\s/gm) ?? []).length;
|
||||
|
||||
if (!stepsMatch) return "standard";
|
||||
|
||||
const stepsIdx = planContent.search(/##\s*Steps/i);
|
||||
const descriptionLength = stepsIdx > 0 ? planContent.slice(0, stepsIdx).length : planContent.length;
|
||||
|
||||
const filePatterns = planContent.match(/`[a-zA-Z0-9_/.-]+\.[a-z]{1,4}`/g) ?? [];
|
||||
const uniqueFiles = new Set(filePatterns.map(f => f.replace(/`/g, "")));
|
||||
const fileCount = uniqueFiles.size;
|
||||
|
||||
const hasComplexitySignals = COMPLEXITY_PATTERN.test(planContent);
|
||||
|
||||
// Count fenced code blocks (from #579 Phase 4)
|
||||
const codeBlockCount = (planContent.match(/^```/gm) ?? []).length / 2;
|
||||
|
||||
if (stepCount >= 8 || fileCount >= 8 || descriptionLength > 2000 || codeBlockCount >= 5) {
|
||||
return "complex";
|
||||
}
|
||||
|
||||
if (stepCount <= 3 && descriptionLength < 500 && fileCount <= 3 && !hasComplexitySignals) {
|
||||
return "simple";
|
||||
}
|
||||
|
||||
return "standard";
|
||||
}
|
||||
|
||||
// ─── Unit Type → Default Tier Mapping (from #579) ─────────────────────────
|
||||
|
||||
const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
|
||||
// Light: structured summaries, completion, UAT
|
||||
"complete-slice": "light",
|
||||
"run-uat": "light",
|
||||
|
||||
// Standard: research, routine planning
|
||||
"research-milestone": "standard",
|
||||
"research-slice": "standard",
|
||||
"plan-milestone": "standard",
|
||||
"plan-slice": "standard",
|
||||
|
||||
// Heavy: execution default (upgraded by metadata), replanning
|
||||
"execute-task": "standard",
|
||||
"replan-slice": "heavy",
|
||||
"reassess-roadmap": "heavy",
|
||||
"validate-milestone": "heavy",
|
||||
"complete-milestone": "standard",
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify unit complexity for model routing.
|
||||
* Uses unit type defaults, task metadata analysis, and budget pressure.
|
||||
*
|
||||
* @param unitType The type of unit being dispatched
|
||||
* @param unitId The unit ID (e.g. "M001/S01/T01")
|
||||
* @param basePath Project base path (for reading task plans)
|
||||
* @param budgetPct Current budget usage as fraction (0.0-1.0+), or undefined
|
||||
* @param metadata Optional pre-parsed task metadata
|
||||
*/
|
||||
export function classifyUnitComplexity(
|
||||
unitType: string,
|
||||
unitId: string,
|
||||
basePath: string,
|
||||
budgetPct?: number,
|
||||
metadata?: TaskMetadata,
|
||||
): ClassificationResult {
|
||||
// Hook units default to light
|
||||
if (unitType.startsWith("hook/")) {
|
||||
return applyBudgetPressure({ tier: "light", reason: "hook unit", downgraded: false }, budgetPct);
|
||||
}
|
||||
|
||||
// Triage/capture units default to light
|
||||
if (unitType === "triage-captures" || unitType.startsWith("quick-task")) {
|
||||
return applyBudgetPressure({ tier: "light", reason: `${unitType} unit`, downgraded: false }, budgetPct);
|
||||
}
|
||||
|
||||
let tier = UNIT_TYPE_TIERS[unitType] ?? "standard";
|
||||
let reason = `unit type: ${unitType}`;
|
||||
|
||||
// For execute-task, analyze task metadata for complexity signals
|
||||
if (unitType === "execute-task") {
|
||||
const analysis = analyzeTaskFromPlan(unitId, basePath, metadata);
|
||||
if (analysis) {
|
||||
tier = analysis.tier;
|
||||
reason = analysis.reason;
|
||||
}
|
||||
}
|
||||
|
||||
return applyBudgetPressure({ tier, reason, downgraded: false }, budgetPct);
|
||||
}
|
||||
|
||||
// ─── Tier Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// tierLabel and tierOrdinal are exported from complexity-classifier.ts (single source)
|
||||
export { tierLabel, tierOrdinal } from "./complexity-classifier.js";
|
||||
|
||||
export function escalateTier(currentTier: ComplexityTier): ComplexityTier | null {
|
||||
switch (currentTier) {
|
||||
case "light": return "standard";
|
||||
case "standard": return "heavy";
|
||||
case "heavy": return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Budget Pressure (from #579 — graduated thresholds) ───────────────────
|
||||
|
||||
function applyBudgetPressure(
|
||||
result: ClassificationResult,
|
||||
budgetPct?: number,
|
||||
): ClassificationResult {
|
||||
if (budgetPct === undefined || budgetPct < 0.5) return result;
|
||||
|
||||
const original = result.tier;
|
||||
|
||||
if (budgetPct >= 0.9) {
|
||||
// >90%: almost everything goes to light
|
||||
if (result.tier !== "heavy") {
|
||||
result.tier = "light";
|
||||
} else {
|
||||
result.tier = "standard";
|
||||
}
|
||||
} else if (budgetPct >= 0.75) {
|
||||
// 75-90%: only heavy stays, standard → light
|
||||
if (result.tier === "standard") {
|
||||
result.tier = "light";
|
||||
}
|
||||
} else {
|
||||
// 50-75%: standard → light
|
||||
if (result.tier === "standard") {
|
||||
result.tier = "light";
|
||||
}
|
||||
}
|
||||
|
||||
if (result.tier !== original) {
|
||||
result.downgraded = true;
|
||||
result.reason = `${result.reason} (budget pressure: ${Math.round(budgetPct * 100)}%)`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Task Plan Analysis ───────────────────────────────────────────────────
|
||||
|
||||
interface TaskAnalysis {
|
||||
tier: ComplexityTier;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
function analyzeTaskFromPlan(
|
||||
unitId: string,
|
||||
basePath: string,
|
||||
metadata?: TaskMetadata,
|
||||
): TaskAnalysis | null {
|
||||
// Try to read the task plan for analysis
|
||||
const parts = unitId.split("/");
|
||||
if (parts.length < 3) return null;
|
||||
|
||||
const [mid, sid, tid] = parts;
|
||||
const planPath = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
|
||||
|
||||
let planContent = "";
|
||||
try {
|
||||
if (existsSync(planPath)) {
|
||||
planContent = readFileSync(planPath, "utf-8");
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!planContent) return null;
|
||||
|
||||
const taskComplexity = classifyTaskComplexity(planContent);
|
||||
|
||||
// Map TaskComplexity to ComplexityTier
|
||||
switch (taskComplexity) {
|
||||
case "simple": return { tier: "light", reason: "task plan: simple (few steps, small scope)" };
|
||||
case "complex": return { tier: "heavy", reason: "task plan: complex (many steps/files or signal words)" };
|
||||
default: return { tier: "standard", reason: "task plan: standard complexity" };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
/**
|
||||
* Complexity Routing — unit tests for M004/S03.
|
||||
*
|
||||
* Tests task complexity classification accuracy and dispatch integration.
|
||||
* Uses direct imports for the classifier (pure function, no heavy deps)
|
||||
* and source-level checks for dispatch/preference wiring.
|
||||
* Tests complexity classification and dispatch integration.
|
||||
* Uses source-level checks for the classifier module and preference wiring.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
|
|
@ -11,183 +10,10 @@ import assert from "node:assert/strict";
|
|||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { classifyTaskComplexity } from "../complexity.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const preferencesSrc = readFileSync(join(__dirname, "..", "preferences.ts"), "utf-8");
|
||||
const complexitySrc = readFileSync(join(__dirname, "..", "complexity.ts"), "utf-8");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Classification: Simple Tasks
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("classify: minimal task plan (2 steps, 1 file) → simple", () => {
|
||||
const plan = `# T01: Add config key
|
||||
|
||||
## Steps
|
||||
1. Add key to interface
|
||||
2. Update validation
|
||||
|
||||
## Files
|
||||
- \`config.ts\`
|
||||
`;
|
||||
assert.equal(classifyTaskComplexity(plan), "simple");
|
||||
});
|
||||
|
||||
test("classify: 3 steps, 2 files, short description → simple", () => {
|
||||
const plan = `# T01: Update types
|
||||
|
||||
Short description.
|
||||
|
||||
## Steps
|
||||
1. Add type
|
||||
2. Export it
|
||||
3. Update imports
|
||||
|
||||
## Files
|
||||
- \`types.ts\`
|
||||
- \`index.ts\`
|
||||
`;
|
||||
assert.equal(classifyTaskComplexity(plan), "simple");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Classification: Standard Tasks
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("classify: medium task plan (5 steps, 4 files) → standard", () => {
|
||||
const plan = `# T02: Implement auth middleware
|
||||
|
||||
Add JWT verification middleware.
|
||||
|
||||
## Steps
|
||||
1. Create middleware file
|
||||
2. Add token verification
|
||||
3. Wire into router
|
||||
4. Add error handling
|
||||
5. Update types
|
||||
|
||||
## Files
|
||||
- \`middleware.ts\`
|
||||
- \`auth.ts\`
|
||||
- \`router.ts\`
|
||||
- \`types.ts\`
|
||||
`;
|
||||
assert.equal(classifyTaskComplexity(plan), "standard");
|
||||
});
|
||||
|
||||
test("classify: 3 steps but complexity signal word → standard (not simple)", () => {
|
||||
const plan = `# T01: Refactor auth
|
||||
|
||||
## Steps
|
||||
1. Extract helper
|
||||
2. Update callers
|
||||
3. Test
|
||||
|
||||
## Files
|
||||
- \`auth.ts\`
|
||||
`;
|
||||
assert.equal(classifyTaskComplexity(plan), "standard");
|
||||
});
|
||||
|
||||
test("classify: 4 steps, short but 4 files → standard", () => {
|
||||
const plan = `# T01: Wire up
|
||||
|
||||
Short.
|
||||
|
||||
## Steps
|
||||
1. Step one
|
||||
2. Step two
|
||||
3. Step three
|
||||
4. Step four
|
||||
|
||||
## Files
|
||||
- \`a.ts\`
|
||||
- \`b.ts\`
|
||||
- \`c.ts\`
|
||||
- \`d.ts\`
|
||||
`;
|
||||
assert.equal(classifyTaskComplexity(plan), "standard");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Classification: Complex Tasks
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("classify: large task plan (10 steps, 8 files) → complex", () => {
|
||||
const plan = `# T03: Migrate database schema
|
||||
|
||||
Full database migration with backward compatibility.
|
||||
|
||||
## Steps
|
||||
1. Create migration file
|
||||
2. Add new columns
|
||||
3. Migrate existing data
|
||||
4. Update ORM models
|
||||
5. Update API handlers
|
||||
6. Update tests
|
||||
7. Run migration locally
|
||||
8. Verify rollback
|
||||
9. Update docs
|
||||
10. Deploy staging
|
||||
|
||||
## Files
|
||||
- \`migrations/001.ts\`
|
||||
- \`models/user.ts\`
|
||||
- \`models/session.ts\`
|
||||
- \`api/users.ts\`
|
||||
- \`api/sessions.ts\`
|
||||
- \`tests/user.test.ts\`
|
||||
- \`tests/session.test.ts\`
|
||||
- \`docs/schema.md\`
|
||||
`;
|
||||
assert.equal(classifyTaskComplexity(plan), "complex");
|
||||
});
|
||||
|
||||
test("classify: long description (>2000 chars) → complex", () => {
|
||||
const longDesc = "A".repeat(2100);
|
||||
const plan = `# T01: Complex task
|
||||
|
||||
${longDesc}
|
||||
|
||||
## Steps
|
||||
|
||||
1. Do it
|
||||
2. Done
|
||||
`;
|
||||
assert.equal(classifyTaskComplexity(plan), "complex");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Classification: Edge Cases
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("classify: empty plan → standard (conservative default)", () => {
|
||||
assert.equal(classifyTaskComplexity(""), "standard");
|
||||
});
|
||||
|
||||
test("classify: plan with no Steps section → standard", () => {
|
||||
const plan = `# T01: Something\n\nJust a description with no structure.\n`;
|
||||
assert.equal(classifyTaskComplexity(plan), "standard");
|
||||
});
|
||||
|
||||
test("classify: null-ish input → standard", () => {
|
||||
assert.equal(classifyTaskComplexity(" "), "standard");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Complexity Signal Words
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("classify: 'investigate' signal prevents simple classification", () => {
|
||||
const plan = `# T01: Investigate auth bug\n\n## Steps\n1. Check logs\n2. Fix\n`;
|
||||
assert.equal(classifyTaskComplexity(plan), "standard");
|
||||
});
|
||||
|
||||
test("classify: 'security' signal prevents simple classification", () => {
|
||||
const plan = `# T01: Security audit\n\n## Steps\n1. Review\n2. Fix\n`;
|
||||
assert.equal(classifyTaskComplexity(plan), "standard");
|
||||
});
|
||||
const complexitySrc = readFileSync(join(__dirname, "..", "complexity-classifier.ts"), "utf-8");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Model Config — execution_simple
|
||||
|
|
@ -218,25 +44,31 @@ test("preferences: resolveModelWithFallbacksForUnit handles execute-task-simple"
|
|||
// Classifier Module Structure
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("complexity: module exports classifyTaskComplexity function", () => {
|
||||
test("complexity: module exports classifyUnitComplexity function", () => {
|
||||
assert.ok(
|
||||
complexitySrc.includes("export function classifyTaskComplexity"),
|
||||
"should export classifyTaskComplexity",
|
||||
complexitySrc.includes("export function classifyUnitComplexity"),
|
||||
"should export classifyUnitComplexity",
|
||||
);
|
||||
});
|
||||
|
||||
test("complexity: module exports TaskComplexity type", () => {
|
||||
test("complexity: module exports ComplexityTier type", () => {
|
||||
assert.ok(
|
||||
complexitySrc.includes("export type TaskComplexity"),
|
||||
"should export TaskComplexity type",
|
||||
complexitySrc.includes("export type ComplexityTier"),
|
||||
"should export ComplexityTier type",
|
||||
);
|
||||
});
|
||||
|
||||
test("complexity: classifier uses conservative defaults", () => {
|
||||
// Verify empty/missing input returns standard
|
||||
test("complexity: module exports tierLabel function", () => {
|
||||
assert.ok(
|
||||
complexitySrc.includes('return "standard"'),
|
||||
"should have standard as default return",
|
||||
complexitySrc.includes("export function tierLabel"),
|
||||
"should export tierLabel for dashboard display",
|
||||
);
|
||||
});
|
||||
|
||||
test("complexity: module exports tierOrdinal function", () => {
|
||||
assert.ok(
|
||||
complexitySrc.includes("export function tierOrdinal"),
|
||||
"should export tierOrdinal for tier comparison",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -244,52 +76,36 @@ test("complexity: classifier uses conservative defaults", () => {
|
|||
// Unit Complexity Classification (from #579 — combined)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const complexitySrcFull = readFileSync(join(__dirname, "..", "complexity.ts"), "utf-8");
|
||||
|
||||
test("unit-classify: classifyUnitComplexity is exported", () => {
|
||||
assert.ok(
|
||||
complexitySrcFull.includes("export function classifyUnitComplexity"),
|
||||
complexitySrc.includes("export function classifyUnitComplexity"),
|
||||
"should export classifyUnitComplexity",
|
||||
);
|
||||
});
|
||||
|
||||
test("unit-classify: unit type tier mapping exists", () => {
|
||||
assert.ok(complexitySrcFull.includes("UNIT_TYPE_TIERS"), "should have unit type tier mapping");
|
||||
assert.ok(complexitySrcFull.includes('"complete-slice": "light"'), "complete-slice should be light");
|
||||
assert.ok(complexitySrcFull.includes('"replan-slice": "heavy"'), "replan-slice should be heavy");
|
||||
assert.ok(complexitySrc.includes("UNIT_TYPE_TIERS"), "should have unit type tier mapping");
|
||||
assert.ok(complexitySrc.includes('"complete-slice": "light"'), "complete-slice should be light");
|
||||
assert.ok(complexitySrc.includes('"replan-slice": "heavy"'), "replan-slice should be heavy");
|
||||
});
|
||||
|
||||
test("unit-classify: hook units default to light", () => {
|
||||
assert.ok(
|
||||
complexitySrcFull.includes('startsWith("hook/")') && complexitySrcFull.includes('"light"'),
|
||||
complexitySrc.includes('startsWith("hook/")') && complexitySrc.includes('"light"'),
|
||||
"hook units should default to light tier",
|
||||
);
|
||||
});
|
||||
|
||||
test("unit-classify: budget pressure has graduated thresholds", () => {
|
||||
assert.ok(complexitySrcFull.includes("budgetPct >= 0.9"), "should have 90% threshold");
|
||||
assert.ok(complexitySrcFull.includes("budgetPct >= 0.75"), "should have 75% threshold");
|
||||
assert.ok(complexitySrcFull.includes("budgetPct < 0.5"), "should skip below 50%");
|
||||
});
|
||||
|
||||
test("unit-classify: escalateTier function exists", () => {
|
||||
assert.ok(
|
||||
complexitySrcFull.includes("export function escalateTier"),
|
||||
"should export escalateTier for failure recovery",
|
||||
);
|
||||
assert.ok(complexitySrc.includes("budgetPct >= 0.9"), "should have 90% threshold");
|
||||
assert.ok(complexitySrc.includes("budgetPct >= 0.75"), "should have 75% threshold");
|
||||
assert.ok(complexitySrc.includes("budgetPct < 0.5"), "should skip below 50%");
|
||||
});
|
||||
|
||||
test("unit-classify: tierLabel function exists", () => {
|
||||
assert.ok(
|
||||
complexitySrcFull.includes("export function tierLabel") ||
|
||||
complexitySrcFull.includes("export { tierLabel"),
|
||||
complexitySrc.includes("export function tierLabel") ||
|
||||
complexitySrc.includes("export { tierLabel"),
|
||||
"should export tierLabel for dashboard display",
|
||||
);
|
||||
});
|
||||
|
||||
test("unit-classify: ComplexityTier imported from types.ts", () => {
|
||||
assert.ok(
|
||||
complexitySrcFull.includes('from "./types.js"') && complexitySrcFull.includes("ComplexityTier"),
|
||||
"should import ComplexityTier from types",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue