@dailephd/my-dev-kit-lab 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -0
- package/benchmarks/contracts/benchmark-project-profiles.json +1199 -0
- package/benchmarks/contracts/todo-behavior.md +70 -0
- package/benchmarks/contracts/todo-benchmark-case.json +227 -0
- package/benchmarks/projects/README.md +34 -0
- package/benchmarks/projects/task-analytics-large-mixed/README.md +1 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/__init__.py +3 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/fixtures.py +6 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/metrics.py +29 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/models.py +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/parser.py +16 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/pipeline.py +9 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/quality.py +8 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/reporting.py +11 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_metrics.py +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_parser.py +15 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_quality.py +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_reporting.py +15 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/package.json +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/index.ts +11 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/analyticsSnapshot.ts +20 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/project.ts +5 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/task.ts +10 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/buildProjectLeaderboard.ts +7 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/formatTaskHealthReport.ts +13 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/buildAnalyticsSnapshot.ts +39 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/completeTask.ts +10 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/createTask.ts +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/listTasksByProject.ts +6 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/projectStore.ts +20 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/taskStore.ts +44 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/projectValidation.ts +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/taskValidation.ts +18 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/buildAnalyticsSnapshot.test.ts +48 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/completeTask.test.ts +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/createTask.test.ts +31 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/listTasksByProject.test.ts +18 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/reporting.test.ts +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tsconfig.json +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/vitest.config.ts +5 -0
- package/benchmarks/projects/task-workflow-medium-ts/README.md +1 -0
- package/benchmarks/projects/task-workflow-medium-ts/package.json +12 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/index.ts +9 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/models/project.ts +6 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/models/task.ts +39 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/completeTask.ts +15 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/createTask.ts +26 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/filterTasks.ts +17 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/importTasks.ts +33 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/summarizeTasks.ts +30 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/store/taskStore.ts +76 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/utils/deterministicId.ts +3 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/validation/taskValidation.ts +45 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/completeTask.test.ts +16 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/createTask.test.ts +21 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/filterTasks.test.ts +18 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/importTasks.test.ts +22 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/summarizeTasks.test.ts +29 -0
- package/benchmarks/projects/task-workflow-medium-ts/tsconfig.json +12 -0
- package/benchmarks/projects/task-workflow-medium-ts/vitest.config.ts +5 -0
- package/benchmarks/projects/todo-js/README.md +3 -0
- package/benchmarks/projects/todo-js/package.json +11 -0
- package/benchmarks/projects/todo-js/src/index.js +2 -0
- package/benchmarks/projects/todo-js/src/taskService.js +37 -0
- package/benchmarks/projects/todo-js/src/taskStore.js +28 -0
- package/benchmarks/projects/todo-js/tests/taskService.test.js +45 -0
- package/benchmarks/projects/todo-js/vitest.config.js +5 -0
- package/benchmarks/projects/todo-mixed-ts-py/README.md +3 -0
- package/benchmarks/projects/todo-mixed-ts-py/package.json +13 -0
- package/benchmarks/projects/todo-mixed-ts-py/python/task_service.py +76 -0
- package/benchmarks/projects/todo-mixed-ts-py/src/taskCli.ts +38 -0
- package/benchmarks/projects/todo-mixed-ts-py/tests/mixedBoundary.test.ts +18 -0
- package/benchmarks/projects/todo-mixed-ts-py/tsconfig.json +12 -0
- package/benchmarks/projects/todo-mixed-ts-py/vitest.config.ts +5 -0
- package/benchmarks/projects/todo-python/README.md +3 -0
- package/benchmarks/projects/todo-python/src/__init__.py +4 -0
- package/benchmarks/projects/todo-python/src/task_service.py +32 -0
- package/benchmarks/projects/todo-python/src/task_store.py +28 -0
- package/benchmarks/projects/todo-python/tests/test_task_service.py +52 -0
- package/benchmarks/projects/todo-ts/README.md +3 -0
- package/benchmarks/projects/todo-ts/package.json +12 -0
- package/benchmarks/projects/todo-ts/src/index.ts +2 -0
- package/benchmarks/projects/todo-ts/src/taskService.ts +41 -0
- package/benchmarks/projects/todo-ts/src/taskStore.ts +34 -0
- package/benchmarks/projects/todo-ts/tests/taskService.test.ts +45 -0
- package/benchmarks/projects/todo-ts/tsconfig.json +12 -0
- package/benchmarks/projects/todo-ts/vitest.config.ts +5 -0
- package/dist/scripts/build-gallery.js +3 -0
- package/dist/scripts/capture-demo-report.js +3 -0
- package/dist/scripts/evaluate-token-savings.js +2 -0
- package/dist/scripts/experiments/describeExperiment.js +143 -0
- package/dist/scripts/experiments/listExperiments.js +44 -0
- package/dist/scripts/experiments/runExperiment.js +199 -0
- package/dist/scripts/generate-experiment-plots.js +3 -0
- package/dist/scripts/generate-prompt-variants.js +2 -0
- package/dist/scripts/render-experiment-report.js +2 -0
- package/dist/scripts/run-agent-prompt.js +2 -0
- package/dist/scripts/run-controlled-experiment.js +2 -0
- package/dist/scripts/run-final-demo.js +3 -0
- package/dist/scripts/run-lab-demo.js +5 -0
- package/dist/scripts/run-visualization-demos.js +3 -0
- package/dist/scripts/security/runCodeql.js +57 -0
- package/dist/scripts/security/runDependencyChecks.js +57 -0
- package/dist/scripts/security/runFuzzSmoke.js +29 -0
- package/dist/scripts/security/runPackageChecks.js +56 -0
- package/dist/scripts/security/runSemgrep.js +63 -0
- package/dist/scripts/security/validate.js +117 -0
- package/dist/scripts/verify-benchmarks.js +202 -0
- package/dist/src/agents/adapters/claudeAdapter.js +37 -0
- package/dist/src/agents/adapters/codexAdapter.js +110 -0
- package/dist/src/agents/adapters/fakeAgentAdapter.js +101 -0
- package/dist/src/agents/agentRegistry.js +21 -0
- package/dist/src/agents/index.js +7 -0
- package/dist/src/agents/parseAgentTokenUsage.js +137 -0
- package/dist/src/agents/runAgentPrompt.js +38 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/commands/buildGalleryCommand.js +56 -0
- package/dist/src/commands/captureDemoReport.js +116 -0
- package/dist/src/commands/evaluateTokenSavings.js +175 -0
- package/dist/src/commands/generateExperimentPlotsCommand.js +38 -0
- package/dist/src/commands/generatePromptVariants.js +67 -0
- package/dist/src/commands/renderExperimentReportCommand.js +131 -0
- package/dist/src/commands/runAgentPromptCommand.js +132 -0
- package/dist/src/commands/runControlledExperimentCommand.js +174 -0
- package/dist/src/commands/runFinalDemoCommand.js +123 -0
- package/dist/src/commands/runLabDemo.js +62 -0
- package/dist/src/commands/runVisualizationDemosCommand.js +67 -0
- package/dist/src/core/commandLine.js +59 -0
- package/dist/src/core/countTokens.js +8 -0
- package/dist/src/core/fileGlobs.js +100 -0
- package/dist/src/core/localProjectTarget.js +75 -0
- package/dist/src/core/pathSafety.js +19 -0
- package/dist/src/core/pythonCommand.js +30 -0
- package/dist/src/core/resolveCommand.js +110 -0
- package/dist/src/core/runMeasuredCommand.js +143 -0
- package/dist/src/evaluation/benchmarkMetadata.js +207 -0
- package/dist/src/evaluation/buildExperimentMatrix.js +75 -0
- package/dist/src/evaluation/classifyAgentRunOutcome.js +40 -0
- package/dist/src/evaluation/compareExperimentRuns.js +79 -0
- package/dist/src/evaluation/compareTokenSavings.js +47 -0
- package/dist/src/evaluation/controlledExperimentTypes.js +1 -0
- package/dist/src/evaluation/index.js +18 -0
- package/dist/src/evaluation/parseAgentAnswer.js +230 -0
- package/dist/src/evaluation/projectComplexity.js +126 -0
- package/dist/src/evaluation/projectFileTree.js +83 -0
- package/dist/src/evaluation/readEvaluationCases.js +59 -0
- package/dist/src/evaluation/renderTokenSavingsReportInput.js +55 -0
- package/dist/src/evaluation/runControlledExperiment.js +158 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +197 -0
- package/dist/src/evaluation/runRawFullFileBaseline.js +31 -0
- package/dist/src/evaluation/scoreCorrectness.js +127 -0
- package/dist/src/evaluation/types.js +1 -0
- package/dist/src/evaluation/writeExperimentArtifacts.js +104 -0
- package/dist/src/evaluation/writeTokenSavingsArtifacts.js +57 -0
- package/dist/src/experiments/config.js +24 -0
- package/dist/src/experiments/defaultRegistry.js +7 -0
- package/dist/src/experiments/errors.js +18 -0
- package/dist/src/experiments/index.js +9 -0
- package/dist/src/experiments/outputPaths.js +25 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/config.js +37 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/index.js +3 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/plugin.js +83 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/resultMapping.js +260 -0
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/registry.js +43 -0
- package/dist/src/experiments/results.js +48 -0
- package/dist/src/experiments/runner.js +181 -0
- package/dist/src/experiments/target.js +8 -0
- package/dist/src/experiments/types.js +1 -0
- package/dist/src/gallery/index.js +2 -0
- package/dist/src/gallery/types.js +1 -0
- package/dist/src/gallery/writeGalleryManifest.js +214 -0
- package/dist/src/index.js +12 -0
- package/dist/src/plots/buildExperimentPlotData.js +137 -0
- package/dist/src/plots/index.js +4 -0
- package/dist/src/plots/renderSvgChart.js +82 -0
- package/dist/src/plots/types.js +1 -0
- package/dist/src/plots/writePlotArtifacts.js +46 -0
- package/dist/src/prompts/buildPromptContext.js +68 -0
- package/dist/src/prompts/generateMyDevKitPrompt.js +106 -0
- package/dist/src/prompts/generatePromptVariants.js +36 -0
- package/dist/src/prompts/generateRawFullFilePrompt.js +97 -0
- package/dist/src/prompts/index.js +7 -0
- package/dist/src/prompts/measurePromptComplexity.js +41 -0
- package/dist/src/prompts/types.js +1 -0
- package/dist/src/prompts/writePromptArtifacts.js +43 -0
- package/dist/src/report/buildExperimentReportInput.js +339 -0
- package/dist/src/report/experimentReportTypes.js +1 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +153 -0
- package/dist/src/report/experiments/experimentReportModel.js +1 -0
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +133 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +30 -0
- package/dist/src/report/index.js +8 -0
- package/dist/src/report/renderExperimentHtmlReport.js +354 -0
- package/dist/src/report/renderHtmlReport.js +103 -0
- package/dist/src/report/types.js +10 -0
- package/dist/src/report/writeExperimentReportArtifacts.js +38 -0
- package/dist/src/report/writeReportArtifacts.js +39 -0
- package/dist/src/screenshot/captureReportScreenshot.js +75 -0
- package/dist/src/screenshot/index.js +2 -0
- package/dist/src/screenshot/types.js +1 -0
- package/dist/src/securityValidation/artifacts.js +15 -0
- package/dist/src/securityValidation/cliAdversarial/adversarialCliConfig.js +38 -0
- package/dist/src/securityValidation/cliAdversarial/dataVolumeChecks.js +194 -0
- package/dist/src/securityValidation/cliAdversarial/jsonStdoutChecks.js +359 -0
- package/dist/src/securityValidation/cliAdversarial/malformedArtifactChecks.js +284 -0
- package/dist/src/securityValidation/cliAdversarial/malformedArtifactFixtures.js +79 -0
- package/dist/src/securityValidation/cliAdversarial/pathBoundaryChecks.js +431 -0
- package/dist/src/securityValidation/cliAdversarial/pathCases.js +144 -0
- package/dist/src/securityValidation/cliAdversarial/readOnlyBoundaryChecks.js +294 -0
- package/dist/src/securityValidation/cliAdversarial/runAdversarialCheck.js +149 -0
- package/dist/src/securityValidation/cliAdversarial/subprocessSafetyChecks.js +214 -0
- package/dist/src/securityValidation/cliAdversarial/tempWorkspace.js +160 -0
- package/dist/src/securityValidation/commandRunner.js +136 -0
- package/dist/src/securityValidation/config.js +39 -0
- package/dist/src/securityValidation/dependencies/parseNpmAudit.js +115 -0
- package/dist/src/securityValidation/dependencies/parseNpmLs.js +71 -0
- package/dist/src/securityValidation/dependencies/parseNpmOutdated.js +41 -0
- package/dist/src/securityValidation/dependencies/runDependencyChecks.js +239 -0
- package/dist/src/securityValidation/dependencies/runOsvScanner.js +43 -0
- package/dist/src/securityValidation/fuzz/fuzzHarness.js +61 -0
- package/dist/src/securityValidation/fuzz/fuzzTargets.js +204 -0
- package/dist/src/securityValidation/fuzz/randomInput.js +0 -0
- package/dist/src/securityValidation/index.js +34 -0
- package/dist/src/securityValidation/packageChecks/forbiddenPackageContents.js +67 -0
- package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +56 -0
- package/dist/src/securityValidation/packageChecks/runPackageChecks.js +88 -0
- package/dist/src/securityValidation/report/renderSecurityReport.js +248 -0
- package/dist/src/securityValidation/report/securityReportTypes.js +1 -0
- package/dist/src/securityValidation/staticScans/codeql.js +66 -0
- package/dist/src/securityValidation/staticScans/semgrep.js +180 -0
- package/dist/src/securityValidation/testMatrix.js +535 -0
- package/dist/src/securityValidation/types.js +34 -0
- package/dist/src/securityValidation/validate/resolveTarget.js +32 -0
- package/dist/src/securityValidation/validate/runSecurityValidation.js +169 -0
- package/dist/src/securityValidation/validate/verdict.js +73 -0
- package/dist/src/visualizationDemos/buildMyDevKitVisualizationCommands.js +59 -0
- package/dist/src/visualizationDemos/index.js +4 -0
- package/dist/src/visualizationDemos/runVisualizationDemos.js +82 -0
- package/dist/src/visualizationDemos/types.js +1 -0
- package/dist/src/visualizationDemos/writeVisualizationDemoArtifacts.js +25 -0
- package/docs/METRICS.md +286 -0
- package/examples/demo-report-input.json +78 -0
- package/examples/lab-demo-cases.json +35 -0
- package/examples/real-agent-campaign-cases.json +118 -0
- package/examples/token-savings-cases.json +122 -0
- package/package.json +91 -0
- package/tests/fixtures/fake-adversarial-cli.js +152 -0
- package/tests/fixtures/fake-my-dev-kit-cli.js +83 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnalyticsTask } from "../models/task.js";
|
|
2
|
+
import { AnalyticsTaskStore } from "../store/taskStore.js";
|
|
3
|
+
|
|
4
|
+
export function completeTask(taskStore: AnalyticsTaskStore, taskId: string, updatedDay: number): AnalyticsTask {
|
|
5
|
+
return taskStore.update(taskId, (task) => ({
|
|
6
|
+
...task,
|
|
7
|
+
completed: true,
|
|
8
|
+
updatedDay
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnalyticsTask } from "../models/task.js";
|
|
2
|
+
import { ProjectStore } from "../store/projectStore.js";
|
|
3
|
+
import { AnalyticsTaskStore } from "../store/taskStore.js";
|
|
4
|
+
import { assertKnownProject } from "../validation/projectValidation.js";
|
|
5
|
+
import { normalizeLabels, validateStoryPoints, validateTaskTitle } from "../validation/taskValidation.js";
|
|
6
|
+
|
|
7
|
+
export function createTask(
|
|
8
|
+
taskStore: AnalyticsTaskStore,
|
|
9
|
+
projectStore: ProjectStore,
|
|
10
|
+
input: Omit<AnalyticsTask, "id" | "completed"> & { completed?: boolean }
|
|
11
|
+
): AnalyticsTask {
|
|
12
|
+
return taskStore.create({
|
|
13
|
+
title: validateTaskTitle(input.title),
|
|
14
|
+
projectId: assertKnownProject(projectStore, input.projectId),
|
|
15
|
+
assignee: input.assignee.trim(),
|
|
16
|
+
completed: input.completed ?? false,
|
|
17
|
+
storyPoints: validateStoryPoints(input.storyPoints),
|
|
18
|
+
updatedDay: input.updatedDay,
|
|
19
|
+
labels: normalizeLabels(input.labels)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnalyticsTask } from "../models/task.js";
|
|
2
|
+
import { AnalyticsTaskStore } from "../store/taskStore.js";
|
|
3
|
+
|
|
4
|
+
export function listTasksByProject(taskStore: AnalyticsTaskStore, projectId: string): AnalyticsTask[] {
|
|
5
|
+
return taskStore.list().filter((task) => task.projectId === projectId);
|
|
6
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnalyticsProject } from "../models/project.js";
|
|
2
|
+
|
|
3
|
+
export class ProjectStore {
|
|
4
|
+
private readonly projects = new Map<string, AnalyticsProject>();
|
|
5
|
+
|
|
6
|
+
constructor(seed: AnalyticsProject[] = []) {
|
|
7
|
+
for (const project of seed) {
|
|
8
|
+
this.projects.set(project.id, { ...project });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
list(): AnalyticsProject[] {
|
|
13
|
+
return [...this.projects.values()].map((project) => ({ ...project }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(projectId: string): AnalyticsProject | undefined {
|
|
17
|
+
const project = this.projects.get(projectId);
|
|
18
|
+
return project ? { ...project } : undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnalyticsTask } from "../models/task.js";
|
|
2
|
+
|
|
3
|
+
export class AnalyticsTaskStore {
|
|
4
|
+
private nextSequence = 1;
|
|
5
|
+
private readonly tasks = new Map<string, AnalyticsTask>();
|
|
6
|
+
|
|
7
|
+
constructor(seed: AnalyticsTask[] = []) {
|
|
8
|
+
for (const task of seed) {
|
|
9
|
+
this.tasks.set(task.id, { ...task, labels: [...task.labels] });
|
|
10
|
+
this.nextSequence = Math.max(this.nextSequence, sequenceFromId(task.id) + 1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
create(input: Omit<AnalyticsTask, "id">): AnalyticsTask {
|
|
15
|
+
const task: AnalyticsTask = {
|
|
16
|
+
...input,
|
|
17
|
+
id: `task-${this.nextSequence++}`,
|
|
18
|
+
labels: [...input.labels]
|
|
19
|
+
};
|
|
20
|
+
this.tasks.set(task.id, task);
|
|
21
|
+
return { ...task, labels: [...task.labels] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
update(taskId: string, updater: (task: AnalyticsTask) => AnalyticsTask): AnalyticsTask {
|
|
25
|
+
const task = this.tasks.get(taskId);
|
|
26
|
+
if (!task) {
|
|
27
|
+
throw new Error(`Unknown task id: ${taskId}`);
|
|
28
|
+
}
|
|
29
|
+
const updated = updater({ ...task, labels: [...task.labels] });
|
|
30
|
+
this.tasks.set(taskId, { ...updated, labels: [...updated.labels] });
|
|
31
|
+
return { ...updated, labels: [...updated.labels] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
list(): AnalyticsTask[] {
|
|
35
|
+
return [...this.tasks.values()]
|
|
36
|
+
.map((task) => ({ ...task, labels: [...task.labels] }))
|
|
37
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sequenceFromId(id: string): number {
|
|
42
|
+
const match = id.match(/-(\d+)$/);
|
|
43
|
+
return match ? Number(match[1]) : 0;
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ProjectStore } from "../store/projectStore.js";
|
|
2
|
+
|
|
3
|
+
export function assertKnownProject(projectStore: ProjectStore, projectId: string): string {
|
|
4
|
+
const normalized = projectId.trim();
|
|
5
|
+
if (!normalized) {
|
|
6
|
+
throw new Error("projectId must not be empty.");
|
|
7
|
+
}
|
|
8
|
+
if (!projectStore.get(normalized)) {
|
|
9
|
+
throw new Error(`Unknown project id: ${normalized}`);
|
|
10
|
+
}
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function validateTaskTitle(title: string): string {
|
|
2
|
+
const normalized = title.trim().replace(/\s+/g, " ");
|
|
3
|
+
if (!normalized) {
|
|
4
|
+
throw new Error("Task title must not be empty.");
|
|
5
|
+
}
|
|
6
|
+
return normalized;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validateStoryPoints(storyPoints: number): number {
|
|
10
|
+
if (!Number.isInteger(storyPoints) || storyPoints < 1 || storyPoints > 13) {
|
|
11
|
+
throw new Error("storyPoints must be an integer between 1 and 13.");
|
|
12
|
+
}
|
|
13
|
+
return storyPoints;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeLabels(labels: string[]): string[] {
|
|
17
|
+
return [...new Set(labels.map((label) => label.trim().toLowerCase()).filter(Boolean))].sort();
|
|
18
|
+
}
|
package/benchmarks/projects/task-analytics-large-mixed/ts/tests/buildAnalyticsSnapshot.test.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildAnalyticsSnapshot } from "../src/services/buildAnalyticsSnapshot.js";
|
|
3
|
+
import { ProjectStore } from "../src/store/projectStore.js";
|
|
4
|
+
import { AnalyticsTaskStore } from "../src/store/taskStore.js";
|
|
5
|
+
import { completeTask } from "../src/services/completeTask.js";
|
|
6
|
+
import { createTask } from "../src/services/createTask.js";
|
|
7
|
+
|
|
8
|
+
describe("buildAnalyticsSnapshot", () => {
|
|
9
|
+
it("computes per-project totals, completion rate, and stale counts", () => {
|
|
10
|
+
const projectStore = new ProjectStore([
|
|
11
|
+
{ id: "alpha", name: "Alpha", owner: "Ada" },
|
|
12
|
+
{ id: "beta", name: "Beta", owner: "Bo" }
|
|
13
|
+
]);
|
|
14
|
+
const taskStore = new AnalyticsTaskStore();
|
|
15
|
+
const done = createTask(taskStore, projectStore, {
|
|
16
|
+
title: "Ship metrics",
|
|
17
|
+
projectId: "alpha",
|
|
18
|
+
assignee: "Lee",
|
|
19
|
+
storyPoints: 5,
|
|
20
|
+
updatedDay: 2,
|
|
21
|
+
labels: []
|
|
22
|
+
});
|
|
23
|
+
createTask(taskStore, projectStore, {
|
|
24
|
+
title: "Audit backlog",
|
|
25
|
+
projectId: "alpha",
|
|
26
|
+
assignee: "Sam",
|
|
27
|
+
storyPoints: 3,
|
|
28
|
+
updatedDay: 1,
|
|
29
|
+
labels: ["ops"]
|
|
30
|
+
});
|
|
31
|
+
createTask(taskStore, projectStore, {
|
|
32
|
+
title: "Tune report",
|
|
33
|
+
projectId: "beta",
|
|
34
|
+
assignee: "Ira",
|
|
35
|
+
storyPoints: 8,
|
|
36
|
+
updatedDay: 11,
|
|
37
|
+
labels: ["report"]
|
|
38
|
+
});
|
|
39
|
+
completeTask(taskStore, done.id, 8);
|
|
40
|
+
expect(buildAnalyticsSnapshot(taskStore, projectStore, 15)).toMatchObject({
|
|
41
|
+
totals: { totalTasks: 3, completedTasks: 1, openTasks: 2, staleTasks: 1 },
|
|
42
|
+
projects: [
|
|
43
|
+
{ projectId: "alpha", totalTasks: 2, completedTasks: 1, openTasks: 1, staleTasks: 1, completionRate: 50 },
|
|
44
|
+
{ projectId: "beta", totalTasks: 1, completedTasks: 0, openTasks: 1, staleTasks: 0, completionRate: 0 }
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ProjectStore } from "../src/store/projectStore.js";
|
|
3
|
+
import { AnalyticsTaskStore } from "../src/store/taskStore.js";
|
|
4
|
+
import { completeTask } from "../src/services/completeTask.js";
|
|
5
|
+
import { createTask } from "../src/services/createTask.js";
|
|
6
|
+
|
|
7
|
+
describe("completeTask", () => {
|
|
8
|
+
it("marks a selected task completed and updates its day marker", () => {
|
|
9
|
+
const projectStore = new ProjectStore([{ id: "alpha", name: "Alpha", owner: "Ada" }]);
|
|
10
|
+
const taskStore = new AnalyticsTaskStore();
|
|
11
|
+
const created = createTask(taskStore, projectStore, {
|
|
12
|
+
title: "Ship metrics",
|
|
13
|
+
projectId: "alpha",
|
|
14
|
+
assignee: "Lee",
|
|
15
|
+
storyPoints: 5,
|
|
16
|
+
updatedDay: 2,
|
|
17
|
+
labels: []
|
|
18
|
+
});
|
|
19
|
+
expect(completeTask(taskStore, created.id, 9)).toMatchObject({ completed: true, updatedDay: 9 });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ProjectStore } from "../src/store/projectStore.js";
|
|
3
|
+
import { AnalyticsTaskStore } from "../src/store/taskStore.js";
|
|
4
|
+
import { createTask } from "../src/services/createTask.js";
|
|
5
|
+
|
|
6
|
+
describe("createTask", () => {
|
|
7
|
+
it("creates deterministic ids and validates projects", () => {
|
|
8
|
+
const projectStore = new ProjectStore([{ id: "alpha", name: "Alpha", owner: "Ada" }]);
|
|
9
|
+
const taskStore = new AnalyticsTaskStore();
|
|
10
|
+
expect(
|
|
11
|
+
createTask(taskStore, projectStore, {
|
|
12
|
+
title: "Ship metrics",
|
|
13
|
+
projectId: "alpha",
|
|
14
|
+
assignee: "Lee",
|
|
15
|
+
storyPoints: 5,
|
|
16
|
+
updatedDay: 2,
|
|
17
|
+
labels: ["Metrics"]
|
|
18
|
+
}).id
|
|
19
|
+
).toBe("task-1");
|
|
20
|
+
expect(() =>
|
|
21
|
+
createTask(taskStore, projectStore, {
|
|
22
|
+
title: "Ship metrics",
|
|
23
|
+
projectId: "missing",
|
|
24
|
+
assignee: "Lee",
|
|
25
|
+
storyPoints: 5,
|
|
26
|
+
updatedDay: 2,
|
|
27
|
+
labels: []
|
|
28
|
+
})
|
|
29
|
+
).toThrow("Unknown project id: missing");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ProjectStore } from "../src/store/projectStore.js";
|
|
3
|
+
import { AnalyticsTaskStore } from "../src/store/taskStore.js";
|
|
4
|
+
import { createTask } from "../src/services/createTask.js";
|
|
5
|
+
import { listTasksByProject } from "../src/services/listTasksByProject.js";
|
|
6
|
+
|
|
7
|
+
describe("listTasksByProject", () => {
|
|
8
|
+
it("returns only tasks for the requested project", () => {
|
|
9
|
+
const projectStore = new ProjectStore([
|
|
10
|
+
{ id: "alpha", name: "Alpha", owner: "Ada" },
|
|
11
|
+
{ id: "beta", name: "Beta", owner: "Bo" }
|
|
12
|
+
]);
|
|
13
|
+
const taskStore = new AnalyticsTaskStore();
|
|
14
|
+
createTask(taskStore, projectStore, { title: "One", projectId: "alpha", assignee: "A", storyPoints: 2, updatedDay: 1, labels: [] });
|
|
15
|
+
createTask(taskStore, projectStore, { title: "Two", projectId: "beta", assignee: "B", storyPoints: 3, updatedDay: 1, labels: [] });
|
|
16
|
+
expect(listTasksByProject(taskStore, "alpha").map((task) => task.title)).toEqual(["One"]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildProjectLeaderboard } from "../src/reporting/buildProjectLeaderboard.js";
|
|
3
|
+
import { formatTaskHealthReport } from "../src/reporting/formatTaskHealthReport.js";
|
|
4
|
+
|
|
5
|
+
const snapshot = {
|
|
6
|
+
generatedAt: "2026-03-15T00:00:00.000Z",
|
|
7
|
+
projects: [
|
|
8
|
+
{ projectId: "alpha", totalTasks: 2, completedTasks: 2, openTasks: 0, staleTasks: 0, averageStoryPoints: 4, completionRate: 100 },
|
|
9
|
+
{ projectId: "beta", totalTasks: 2, completedTasks: 1, openTasks: 1, staleTasks: 1, averageStoryPoints: 5, completionRate: 50 }
|
|
10
|
+
],
|
|
11
|
+
totals: { totalTasks: 4, completedTasks: 3, openTasks: 1, staleTasks: 1 }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("reporting", () => {
|
|
15
|
+
it("ranks projects and formats a deterministic report", () => {
|
|
16
|
+
expect(buildProjectLeaderboard(snapshot)).toEqual(["1. alpha (100% complete, 0 stale)", "2. beta (50% complete, 1 stale)"]);
|
|
17
|
+
expect(formatTaskHealthReport(snapshot)).toContain("Projects: 1. alpha (100% complete, 0 stale) | 2. beta (50% complete, 1 stale)");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["vitest/globals"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Task workflow benchmark project with multiple services, validation rules, filtering, summarization, and deterministic imports.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./models/project.js";
|
|
2
|
+
export * from "./models/task.js";
|
|
3
|
+
export * from "./services/completeTask.js";
|
|
4
|
+
export * from "./services/createTask.js";
|
|
5
|
+
export * from "./services/filterTasks.js";
|
|
6
|
+
export * from "./services/importTasks.js";
|
|
7
|
+
export * from "./services/summarizeTasks.js";
|
|
8
|
+
export * from "./store/taskStore.js";
|
|
9
|
+
export * from "./validation/taskValidation.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type TaskPriority = "low" | "medium" | "high";
|
|
2
|
+
|
|
3
|
+
export type WorkflowTask = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
normalizedTitle: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
priority: TaskPriority;
|
|
9
|
+
tags: string[];
|
|
10
|
+
completed: boolean;
|
|
11
|
+
notes: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
completedAt?: string;
|
|
14
|
+
importSource?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type TaskFilter = {
|
|
18
|
+
projectId?: string;
|
|
19
|
+
query?: string;
|
|
20
|
+
tag?: string;
|
|
21
|
+
completed?: boolean;
|
|
22
|
+
priority?: TaskPriority;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type TaskSummary = {
|
|
26
|
+
total: number;
|
|
27
|
+
completed: number;
|
|
28
|
+
open: number;
|
|
29
|
+
byProject: Record<string, { total: number; completed: number; open: number }>;
|
|
30
|
+
highPriorityOpenTitles: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type TaskImportInput = {
|
|
34
|
+
title: string;
|
|
35
|
+
projectId: string;
|
|
36
|
+
priority?: TaskPriority;
|
|
37
|
+
tags?: string[];
|
|
38
|
+
notes?: string;
|
|
39
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WorkflowTask } from "../models/task.js";
|
|
2
|
+
import { TaskWorkflowStore } from "../store/taskStore.js";
|
|
3
|
+
|
|
4
|
+
export function completeTask(store: TaskWorkflowStore, taskId: string): WorkflowTask {
|
|
5
|
+
return store.updateTask(taskId, (task) => {
|
|
6
|
+
if (task.completed) {
|
|
7
|
+
return task;
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
...task,
|
|
11
|
+
completed: true,
|
|
12
|
+
completedAt: "2026-02-01T00:00:00.000Z"
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TaskPriority, WorkflowTask } from "../models/task.js";
|
|
2
|
+
import { TaskWorkflowStore } from "../store/taskStore.js";
|
|
3
|
+
import { normalizeTags, validatePriority, validateProjectId, validateTaskTitle } from "../validation/taskValidation.js";
|
|
4
|
+
|
|
5
|
+
export type CreateTaskInput = {
|
|
6
|
+
title: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
priority?: TaskPriority;
|
|
9
|
+
tags?: string[];
|
|
10
|
+
notes?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createTask(store: TaskWorkflowStore, input: CreateTaskInput): WorkflowTask {
|
|
14
|
+
const title = validateTaskTitle(input.title);
|
|
15
|
+
const projectId = validateProjectId(input.projectId);
|
|
16
|
+
if (!store.getProject(projectId)) {
|
|
17
|
+
throw new Error(`Unknown project id: ${projectId}`);
|
|
18
|
+
}
|
|
19
|
+
return store.createTask({
|
|
20
|
+
title,
|
|
21
|
+
projectId,
|
|
22
|
+
priority: validatePriority(input.priority),
|
|
23
|
+
tags: normalizeTags(input.tags ?? []),
|
|
24
|
+
notes: input.notes?.trim() ?? ""
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TaskFilter, WorkflowTask } from "../models/task.js";
|
|
2
|
+
import { TaskWorkflowStore } from "../store/taskStore.js";
|
|
3
|
+
|
|
4
|
+
export function filterTasks(store: TaskWorkflowStore, filter: TaskFilter): WorkflowTask[] {
|
|
5
|
+
const query = filter.query?.trim().toLowerCase();
|
|
6
|
+
return store.listTasks().filter((task) => {
|
|
7
|
+
if (filter.projectId && task.projectId !== filter.projectId) return false;
|
|
8
|
+
if (filter.completed !== undefined && task.completed !== filter.completed) return false;
|
|
9
|
+
if (filter.priority && task.priority !== filter.priority) return false;
|
|
10
|
+
if (filter.tag && !task.tags.includes(filter.tag.trim().toLowerCase())) return false;
|
|
11
|
+
if (query) {
|
|
12
|
+
const haystack = `${task.title} ${task.notes} ${task.tags.join(" ")}`.toLowerCase();
|
|
13
|
+
if (!haystack.includes(query)) return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TaskImportInput, WorkflowTask } from "../models/task.js";
|
|
2
|
+
import { TaskWorkflowStore } from "../store/taskStore.js";
|
|
3
|
+
import { validateImportInput } from "../validation/taskValidation.js";
|
|
4
|
+
|
|
5
|
+
export type ImportTasksResult = {
|
|
6
|
+
imported: WorkflowTask[];
|
|
7
|
+
skippedDuplicates: string[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function importTasks(store: TaskWorkflowStore, items: TaskImportInput[], importSource: string): ImportTasksResult {
|
|
11
|
+
const imported: WorkflowTask[] = [];
|
|
12
|
+
const skippedDuplicates: string[] = [];
|
|
13
|
+
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const validated = validateImportInput(item);
|
|
16
|
+
if (!store.getProject(validated.projectId)) {
|
|
17
|
+
throw new Error(`Unknown project id: ${validated.projectId}`);
|
|
18
|
+
}
|
|
19
|
+
const duplicate = store.findDuplicate(validated);
|
|
20
|
+
if (duplicate) {
|
|
21
|
+
skippedDuplicates.push(duplicate.id);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
imported.push(
|
|
25
|
+
store.createTask({
|
|
26
|
+
...validated,
|
|
27
|
+
importSource
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { imported, skippedDuplicates };
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TaskSummary } from "../models/task.js";
|
|
2
|
+
import { TaskWorkflowStore } from "../store/taskStore.js";
|
|
3
|
+
|
|
4
|
+
export function summarizeTasks(store: TaskWorkflowStore): TaskSummary {
|
|
5
|
+
const tasks = store.listTasks();
|
|
6
|
+
const byProject: TaskSummary["byProject"] = {};
|
|
7
|
+
|
|
8
|
+
for (const project of store.listProjects()) {
|
|
9
|
+
byProject[project.id] = { total: 0, completed: 0, open: 0 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (const task of tasks) {
|
|
13
|
+
byProject[task.projectId] ??= { total: 0, completed: 0, open: 0 };
|
|
14
|
+
byProject[task.projectId].total += 1;
|
|
15
|
+
if (task.completed) {
|
|
16
|
+
byProject[task.projectId].completed += 1;
|
|
17
|
+
} else {
|
|
18
|
+
byProject[task.projectId].open += 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const completed = tasks.filter((task) => task.completed).length;
|
|
23
|
+
return {
|
|
24
|
+
total: tasks.length,
|
|
25
|
+
completed,
|
|
26
|
+
open: tasks.length - completed,
|
|
27
|
+
byProject,
|
|
28
|
+
highPriorityOpenTitles: tasks.filter((task) => !task.completed && task.priority === "high").map((task) => task.title)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createDeterministicId } from "../utils/deterministicId.js";
|
|
2
|
+
import type { WorkflowProject } from "../models/project.js";
|
|
3
|
+
import type { TaskImportInput, TaskPriority, WorkflowTask } from "../models/task.js";
|
|
4
|
+
import { normalizeTaskTitle, normalizeTags } from "../validation/taskValidation.js";
|
|
5
|
+
|
|
6
|
+
export class TaskWorkflowStore {
|
|
7
|
+
private nextTaskSequence = 1;
|
|
8
|
+
private readonly projects = new Map<string, WorkflowProject>();
|
|
9
|
+
private readonly tasks = new Map<string, WorkflowTask>();
|
|
10
|
+
|
|
11
|
+
constructor(seed?: { projects?: WorkflowProject[]; tasks?: WorkflowTask[] }) {
|
|
12
|
+
for (const project of seed?.projects ?? []) {
|
|
13
|
+
this.projects.set(project.id, { ...project });
|
|
14
|
+
}
|
|
15
|
+
for (const task of seed?.tasks ?? []) {
|
|
16
|
+
this.tasks.set(task.id, { ...task, tags: [...task.tags] });
|
|
17
|
+
this.nextTaskSequence = Math.max(this.nextTaskSequence, extractSequence(task.id) + 1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addProject(project: WorkflowProject): void {
|
|
22
|
+
this.projects.set(project.id, { ...project });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getProject(projectId: string): WorkflowProject | undefined {
|
|
26
|
+
const project = this.projects.get(projectId);
|
|
27
|
+
return project ? { ...project } : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
listProjects(): WorkflowProject[] {
|
|
31
|
+
return [...this.projects.values()].map((project) => ({ ...project }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
createTask(input: { title: string; projectId: string; priority: TaskPriority; tags: string[]; notes: string; importSource?: string }): WorkflowTask {
|
|
35
|
+
const task: WorkflowTask = {
|
|
36
|
+
id: createDeterministicId("task", this.nextTaskSequence++),
|
|
37
|
+
title: input.title,
|
|
38
|
+
normalizedTitle: normalizeTaskTitle(input.title).toLowerCase(),
|
|
39
|
+
projectId: input.projectId,
|
|
40
|
+
priority: input.priority,
|
|
41
|
+
tags: normalizeTags(input.tags),
|
|
42
|
+
completed: false,
|
|
43
|
+
notes: input.notes,
|
|
44
|
+
createdAt: `2026-01-${String(this.tasks.size + 1).padStart(2, "0")}T00:00:00.000Z`,
|
|
45
|
+
importSource: input.importSource
|
|
46
|
+
};
|
|
47
|
+
this.tasks.set(task.id, task);
|
|
48
|
+
return { ...task, tags: [...task.tags] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
updateTask(id: string, updater: (task: WorkflowTask) => WorkflowTask): WorkflowTask {
|
|
52
|
+
const existing = this.tasks.get(id);
|
|
53
|
+
if (!existing) {
|
|
54
|
+
throw new Error(`Unknown task id: ${id}`);
|
|
55
|
+
}
|
|
56
|
+
const updated = updater({ ...existing, tags: [...existing.tags] });
|
|
57
|
+
this.tasks.set(id, { ...updated, tags: [...updated.tags] });
|
|
58
|
+
return { ...updated, tags: [...updated.tags] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
listTasks(): WorkflowTask[] {
|
|
62
|
+
return [...this.tasks.values()]
|
|
63
|
+
.map((task) => ({ ...task, tags: [...task.tags] }))
|
|
64
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
findDuplicate(input: TaskImportInput): WorkflowTask | undefined {
|
|
68
|
+
const normalizedTitle = normalizeTaskTitle(input.title).toLowerCase();
|
|
69
|
+
return this.listTasks().find((task) => task.projectId === input.projectId && task.normalizedTitle === normalizedTitle);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractSequence(id: string): number {
|
|
74
|
+
const match = id.match(/-(\d+)$/);
|
|
75
|
+
return match ? Number(match[1]) : 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TaskImportInput, TaskPriority } from "../models/task.js";
|
|
2
|
+
|
|
3
|
+
const VALID_PRIORITIES = new Set<TaskPriority>(["low", "medium", "high"]);
|
|
4
|
+
|
|
5
|
+
export function normalizeTaskTitle(title: string): string {
|
|
6
|
+
return title.trim().replace(/\s+/g, " ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validateTaskTitle(title: string): string {
|
|
10
|
+
const normalized = normalizeTaskTitle(title);
|
|
11
|
+
if (!normalized) {
|
|
12
|
+
throw new Error("Task title must not be empty.");
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateProjectId(projectId: string): string {
|
|
18
|
+
const normalized = projectId.trim();
|
|
19
|
+
if (!normalized) {
|
|
20
|
+
throw new Error("projectId must not be empty.");
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validatePriority(priority: TaskPriority | undefined): TaskPriority {
|
|
26
|
+
const resolved = priority ?? "medium";
|
|
27
|
+
if (!VALID_PRIORITIES.has(resolved)) {
|
|
28
|
+
throw new Error(`Unsupported priority: ${priority}`);
|
|
29
|
+
}
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateImportInput(input: TaskImportInput): Required<TaskImportInput> {
|
|
34
|
+
return {
|
|
35
|
+
title: validateTaskTitle(input.title),
|
|
36
|
+
projectId: validateProjectId(input.projectId),
|
|
37
|
+
priority: validatePriority(input.priority),
|
|
38
|
+
tags: normalizeTags(input.tags ?? []),
|
|
39
|
+
notes: input.notes?.trim() ?? ""
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeTags(tags: string[]): string[] {
|
|
44
|
+
return [...new Set(tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean))].sort();
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { completeTask } from "../src/services/completeTask.js";
|
|
3
|
+
import { createTask } from "../src/services/createTask.js";
|
|
4
|
+
import { TaskWorkflowStore } from "../src/store/taskStore.js";
|
|
5
|
+
|
|
6
|
+
describe("completeTask", () => {
|
|
7
|
+
it("marks only the selected task as completed", () => {
|
|
8
|
+
const store = new TaskWorkflowStore({
|
|
9
|
+
projects: [{ id: "alpha", name: "Alpha", slug: "alpha", archived: false }]
|
|
10
|
+
});
|
|
11
|
+
const first = createTask(store, { title: "One", projectId: "alpha" });
|
|
12
|
+
createTask(store, { title: "Two", projectId: "alpha" });
|
|
13
|
+
expect(completeTask(store, first.id).completed).toBe(true);
|
|
14
|
+
expect(store.listTasks().map((task) => task.completed)).toEqual([true, false]);
|
|
15
|
+
});
|
|
16
|
+
});
|