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:
TÂCHES 2026-03-17 18:06:58 -06:00 committed by GitHub
parent 13f9d5585d
commit 47cff561f0
2 changed files with 29 additions and 437 deletions

View file

@ -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" };
}
}

View file

@ -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",
);
});