From 88acf30296fa0a1f2b88ae10e4dd78cdf9971d85 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Thu, 25 Jun 2026 14:50:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(AI):=20=E5=A2=9E=E5=8A=A0=E4=B8=A5?= =?UTF-8?q?=E6=A0=BC=E4=BC=B0=E6=97=B6=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/lib/ai-estimation-policy.test.ts | 25 +++++++++++++ apps/web/lib/ai-estimation-policy.ts | 43 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 apps/web/lib/ai-estimation-policy.test.ts create mode 100644 apps/web/lib/ai-estimation-policy.ts diff --git a/apps/web/lib/ai-estimation-policy.test.ts b/apps/web/lib/ai-estimation-policy.test.ts new file mode 100644 index 0000000..a057dd6 --- /dev/null +++ b/apps/web/lib/ai-estimation-policy.test.ts @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + clampDevEstimateHours, + clampTestCaseEstimateHours, + getDefaultTestCaseEstimateHours, +} from './ai-estimation-policy'; + +test('clamps simple frontend interaction to AI-assisted range', () => { + assert.equal(clampDevEstimateHours('frontend_interaction', 5), 1); + assert.equal(clampDevEstimateHours('frontend_interaction', 0.1), 0.5); +}); + +test('keeps backend API within strict range', () => { + assert.equal(clampDevEstimateHours('backend_api', 0.25), 0.75); + assert.equal(clampDevEstimateHours('backend_api', 2), 1.5); +}); + +test('defaults and clamps test case estimates', () => { + assert.equal(getDefaultTestCaseEstimateHours('test_functional'), 0.5); + assert.equal(clampTestCaseEstimateHours('test_functional', 2), 0.5); + assert.equal(clampTestCaseEstimateHours('test_api', 0.25), 0.5); + assert.equal(clampTestCaseEstimateHours('test_exception', 2), 1); +}); diff --git a/apps/web/lib/ai-estimation-policy.ts b/apps/web/lib/ai-estimation-policy.ts new file mode 100644 index 0000000..05b5809 --- /dev/null +++ b/apps/web/lib/ai-estimation-policy.ts @@ -0,0 +1,43 @@ +import type { AgentTaskCategoryCode } from '@ftb/shared'; + +type EstimateRange = { min: number; max: number; fallback: number }; + +const DEV_ESTIMATE_RANGES: Record = { + frontend_development: { min: 0.5, max: 1.5, fallback: 1 }, + frontend_interaction: { min: 0.5, max: 1, fallback: 0.5 }, + backend_development: { min: 1, max: 3, fallback: 2 }, + backend_api: { min: 0.75, max: 1.5, fallback: 1 }, + database_schema: { min: 0.5, max: 0.5, fallback: 0.5 }, + api_integration: { min: 0.75, max: 1.5, fallback: 1 }, + data_processing: { min: 1, max: 2, fallback: 1.5 }, + implementation_support: { min: 0.5, max: 1.5, fallback: 1 }, + documentation: { min: 0.25, max: 0.5, fallback: 0.5 }, +}; + +const TEST_ESTIMATE_RANGES: Record = { + test_functional: { min: 0.25, max: 0.5, fallback: 0.5 }, + test_api: { min: 0.5, max: 1, fallback: 0.5 }, + test_exception: { min: 0.5, max: 1, fallback: 0.5 }, + test_compatibility: { min: 0.5, max: 1, fallback: 1 }, +}; + +function roundQuarterHour(hours: number): number { + return Math.round(hours * 4) / 4; +} + +function clampToRange(raw: number | undefined, range: EstimateRange): number { + const base = typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? raw : range.fallback; + return roundQuarterHour(Math.min(range.max, Math.max(range.min, base))); +} + +export function clampDevEstimateHours(code: AgentTaskCategoryCode | string | undefined, raw: number | undefined): number { + return clampToRange(raw, DEV_ESTIMATE_RANGES[code ?? ''] ?? { min: 0.5, max: 2, fallback: 1 }); +} + +export function getDefaultTestCaseEstimateHours(code: AgentTaskCategoryCode | string | undefined): number { + return (TEST_ESTIMATE_RANGES[code ?? ''] ?? TEST_ESTIMATE_RANGES.test_functional).fallback; +} + +export function clampTestCaseEstimateHours(code: AgentTaskCategoryCode | string | undefined, raw: number | undefined): number { + return clampToRange(raw, TEST_ESTIMATE_RANGES[code ?? ''] ?? TEST_ESTIMATE_RANGES.test_functional); +}