From 47cff561f0b7cd5ef5db4f95cc2cc2c603a4ba32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Tue, 17 Mar 2026 18:06:58 -0600 Subject: [PATCH] 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) * 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) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/complexity.ts | 224 ---------------- .../gsd/tests/complexity-routing.test.ts | 242 +++--------------- 2 files changed, 29 insertions(+), 437 deletions(-) delete mode 100644 src/resources/extensions/gsd/complexity.ts diff --git a/src/resources/extensions/gsd/complexity.ts b/src/resources/extensions/gsd/complexity.ts deleted file mode 100644 index 72f9b3009..000000000 --- a/src/resources/extensions/gsd/complexity.ts +++ /dev/null @@ -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 = { - // 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" }; - } -} diff --git a/src/resources/extensions/gsd/tests/complexity-routing.test.ts b/src/resources/extensions/gsd/tests/complexity-routing.test.ts index b9d50e3c9..1d3ab24a7 100644 --- a/src/resources/extensions/gsd/tests/complexity-routing.test.ts +++ b/src/resources/extensions/gsd/tests/complexity-routing.test.ts @@ -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", - ); -});