popeye-cli 1.2.1 → 1.4.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/.env.example +4 -1
- package/CONTRIBUTING.md +10 -0
- package/README.md +224 -17
- package/dist/adapters/claude.d.ts +3 -2
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +214 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +2 -2
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/grok.d.ts +2 -1
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai.d.ts +2 -2
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +25 -5
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +5 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +354 -28
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/schema.d.ts +4 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/all.d.ts +70 -0
- package/dist/generators/all.d.ts.map +1 -0
- package/dist/generators/all.js +826 -0
- package/dist/generators/all.js.map +1 -0
- package/dist/generators/fullstack.d.ts +9 -0
- package/dist/generators/fullstack.d.ts.map +1 -1
- package/dist/generators/fullstack.js.map +1 -1
- package/dist/generators/index.d.ts +3 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +33 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/templates/index.d.ts +2 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +2 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website.d.ts +85 -0
- package/dist/generators/templates/website.d.ts.map +1 -0
- package/dist/generators/templates/website.js +877 -0
- package/dist/generators/templates/website.js.map +1 -0
- package/dist/generators/website.d.ts +56 -0
- package/dist/generators/website.d.ts.map +1 -0
- package/dist/generators/website.js +269 -0
- package/dist/generators/website.js.map +1 -0
- package/dist/types/consensus.d.ts +18 -23
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +8 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/project.d.ts +130 -17
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +55 -8
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +2 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +2 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/context.d.ts +37 -0
- package/dist/upgrade/context.d.ts.map +1 -0
- package/dist/upgrade/context.js +284 -0
- package/dist/upgrade/context.js.map +1 -0
- package/dist/upgrade/handlers.d.ts +103 -0
- package/dist/upgrade/handlers.d.ts.map +1 -0
- package/dist/upgrade/handlers.js +384 -0
- package/dist/upgrade/handlers.js.map +1 -0
- package/dist/upgrade/index.d.ts +26 -0
- package/dist/upgrade/index.d.ts.map +1 -0
- package/dist/upgrade/index.js +194 -0
- package/dist/upgrade/index.js.map +1 -0
- package/dist/upgrade/transitions.d.ts +34 -0
- package/dist/upgrade/transitions.d.ts.map +1 -0
- package/dist/upgrade/transitions.js +56 -0
- package/dist/upgrade/transitions.js.map +1 -0
- package/dist/workflow/consensus.d.ts +2 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/index.d.ts +6 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +3 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +41 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-parser.d.ts +97 -0
- package/dist/workflow/plan-parser.d.ts.map +1 -0
- package/dist/workflow/plan-parser.js +235 -0
- package/dist/workflow/plan-parser.js.map +1 -0
- package/dist/workflow/plan-storage.d.ts +40 -12
- package/dist/workflow/plan-storage.d.ts.map +1 -1
- package/dist/workflow/plan-storage.js +47 -20
- package/dist/workflow/plan-storage.js.map +1 -1
- package/dist/workflow/seo-tests.d.ts +43 -0
- package/dist/workflow/seo-tests.d.ts.map +1 -0
- package/dist/workflow/seo-tests.js +192 -0
- package/dist/workflow/seo-tests.js.map +1 -0
- package/dist/workflow/separation-guard.d.ts +35 -0
- package/dist/workflow/separation-guard.d.ts.map +1 -0
- package/dist/workflow/separation-guard.js +154 -0
- package/dist/workflow/separation-guard.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +3 -2
- package/dist/workflow/task-workflow.js.map +1 -1
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +128 -0
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/workspace-manager.d.ts +31 -20
- package/dist/workflow/workspace-manager.d.ts.map +1 -1
- package/dist/workflow/workspace-manager.js +38 -9
- package/dist/workflow/workspace-manager.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude.ts +221 -4
- package/src/adapters/gemini.ts +2 -2
- package/src/adapters/grok.ts +2 -1
- package/src/adapters/index.ts +15 -0
- package/src/adapters/openai.ts +2 -2
- package/src/cli/commands/create.ts +25 -5
- package/src/cli/index.ts +5 -2
- package/src/cli/interactive.ts +400 -29
- package/src/config/schema.ts +2 -1
- package/src/generators/all.ts +897 -0
- package/src/generators/fullstack.ts +10 -0
- package/src/generators/index.ts +54 -0
- package/src/generators/templates/index.ts +2 -0
- package/src/generators/templates/website.ts +906 -0
- package/src/generators/website.ts +350 -0
- package/src/types/consensus.ts +20 -8
- package/src/types/index.ts +35 -0
- package/src/types/project.ts +157 -11
- package/src/types/workflow.ts +2 -1
- package/src/upgrade/context.ts +332 -0
- package/src/upgrade/handlers.ts +477 -0
- package/src/upgrade/index.ts +244 -0
- package/src/upgrade/transitions.ts +80 -0
- package/src/workflow/consensus.ts +3 -2
- package/src/workflow/index.ts +8 -0
- package/src/workflow/plan-mode.ts +44 -10
- package/src/workflow/plan-parser.ts +317 -0
- package/src/workflow/plan-storage.ts +69 -30
- package/src/workflow/seo-tests.ts +246 -0
- package/src/workflow/separation-guard.ts +200 -0
- package/src/workflow/task-workflow.ts +3 -2
- package/src/workflow/test-runner.ts +149 -0
- package/src/workflow/workspace-manager.ts +68 -31
- package/tests/cli/model-command.test.ts +93 -0
- package/tests/types/project.test.ts +90 -15
- package/tests/types/workflow-schema.test.ts +59 -0
- package/tests/upgrade/context.test.ts +211 -0
- package/tests/upgrade/transitions.test.ts +85 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project type upgrade transitions
|
|
3
|
+
* Defines valid upgrade paths between project types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { languageToApps } from '../types/project.js';
|
|
7
|
+
import type { OutputLanguage, AppType } from '../types/project.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Details of a project type transition
|
|
11
|
+
*/
|
|
12
|
+
export interface UpgradeTransition {
|
|
13
|
+
from: OutputLanguage;
|
|
14
|
+
to: OutputLanguage;
|
|
15
|
+
/** New apps that will be generated */
|
|
16
|
+
newApps: AppType[];
|
|
17
|
+
/** Whether existing code needs to be moved into apps/ directory */
|
|
18
|
+
requiresRestructure: boolean;
|
|
19
|
+
/** Description of what the upgrade does */
|
|
20
|
+
description: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get valid upgrade targets for a given language
|
|
25
|
+
*
|
|
26
|
+
* @param from - Current project language
|
|
27
|
+
* @returns Array of valid target languages
|
|
28
|
+
*/
|
|
29
|
+
export function getValidUpgradeTargets(from: OutputLanguage): OutputLanguage[] {
|
|
30
|
+
const targets: Record<OutputLanguage, OutputLanguage[]> = {
|
|
31
|
+
python: ['fullstack', 'all'],
|
|
32
|
+
typescript: ['fullstack', 'all'],
|
|
33
|
+
fullstack: ['all'],
|
|
34
|
+
website: ['all'],
|
|
35
|
+
all: [],
|
|
36
|
+
};
|
|
37
|
+
return targets[from];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get detailed transition information for an upgrade
|
|
42
|
+
*
|
|
43
|
+
* @param from - Current project language
|
|
44
|
+
* @param to - Target project language
|
|
45
|
+
* @returns Transition details or null if invalid
|
|
46
|
+
*/
|
|
47
|
+
export function getTransitionDetails(
|
|
48
|
+
from: OutputLanguage,
|
|
49
|
+
to: OutputLanguage,
|
|
50
|
+
): UpgradeTransition | null {
|
|
51
|
+
const validTargets = getValidUpgradeTargets(from);
|
|
52
|
+
if (!validTargets.includes(to)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const currentApps = new Set(languageToApps(from));
|
|
57
|
+
const targetApps = languageToApps(to);
|
|
58
|
+
const newApps = targetApps.filter((app) => !currentApps.has(app));
|
|
59
|
+
|
|
60
|
+
// Single-app types need restructuring into apps/ monorepo layout
|
|
61
|
+
const singleAppTypes: OutputLanguage[] = ['python', 'typescript', 'website'];
|
|
62
|
+
const requiresRestructure = singleAppTypes.includes(from);
|
|
63
|
+
|
|
64
|
+
const descriptions: Record<string, string> = {
|
|
65
|
+
'python->fullstack': 'Add frontend app, move backend to apps/backend/',
|
|
66
|
+
'python->all': 'Add frontend + website, move backend to apps/backend/',
|
|
67
|
+
'typescript->fullstack': 'Add backend app, move frontend to apps/frontend/',
|
|
68
|
+
'typescript->all': 'Add backend + website, move frontend to apps/frontend/',
|
|
69
|
+
'fullstack->all': 'Add website app to existing workspace',
|
|
70
|
+
'website->all': 'Add frontend + backend, move website to apps/website/',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
from,
|
|
75
|
+
to,
|
|
76
|
+
newApps,
|
|
77
|
+
requiresRestructure,
|
|
78
|
+
description: descriptions[`${from}->${to}`] || `Upgrade from ${from} to ${to}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
AppConsensusScores,
|
|
15
15
|
CorrectionRecord,
|
|
16
16
|
} from '../types/consensus.js';
|
|
17
|
+
import type { OutputLanguage } from '../types/project.js';
|
|
17
18
|
import { DEFAULT_CONSENSUS_CONFIG } from '../types/consensus.js';
|
|
18
19
|
import { requestConsensus as requestOpenAIConsensus } from '../adapters/openai.js';
|
|
19
20
|
import { requestConsensus as requestGeminiConsensus, requestArbitration as requestGeminiArbitration } from '../adapters/gemini.js';
|
|
@@ -36,7 +37,7 @@ export interface ConsensusOptions {
|
|
|
36
37
|
/** Whether this is a fullstack project (enables per-app tracking) */
|
|
37
38
|
isFullstack?: boolean;
|
|
38
39
|
/** Project language for revision prompts */
|
|
39
|
-
language?:
|
|
40
|
+
language?: OutputLanguage;
|
|
40
41
|
onIteration?: (iteration: number, result: ConsensusResult) => void;
|
|
41
42
|
onRevision?: (iteration: number, revisedPlan: string) => void;
|
|
42
43
|
onConcerns?: (concerns: string[], recommendations: string[]) => void;
|
|
@@ -947,7 +948,7 @@ export async function runOptimizedConsensusProcess(
|
|
|
947
948
|
} = options;
|
|
948
949
|
|
|
949
950
|
// Derive language from isFullstack for revision prompts
|
|
950
|
-
const language:
|
|
951
|
+
const language: OutputLanguage = isFullstack ? 'fullstack' : 'python';
|
|
951
952
|
|
|
952
953
|
const {
|
|
953
954
|
threshold = DEFAULT_CONSENSUS_CONFIG.threshold,
|
package/src/workflow/index.ts
CHANGED
|
@@ -41,6 +41,14 @@ export * from './ui-designer.js';
|
|
|
41
41
|
export * from './ui-verification.js';
|
|
42
42
|
export * from './project-verification.js';
|
|
43
43
|
export * from './auto-fix.js';
|
|
44
|
+
// Note: plan-parser.js exports are accessible but have naming conflicts with plan-mode.js
|
|
45
|
+
// Import directly from './plan-parser.js' if you need the extended TaskAppTag type (includes 'WEB')
|
|
46
|
+
export * from './separation-guard.js';
|
|
47
|
+
export * from './seo-tests.js';
|
|
48
|
+
export * from './task-workflow.js';
|
|
49
|
+
export * from './milestone-workflow.js';
|
|
50
|
+
export * from './plan-storage.js';
|
|
51
|
+
export * from './workspace-manager.js';
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
54
|
* Workflow options
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { promises as fs } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import
|
|
8
|
+
import { isWorkspace } from '../types/project.js';
|
|
9
|
+
import type { ProjectSpec, OutputLanguage } from '../types/project.js';
|
|
9
10
|
import type { ProjectState, Milestone, Task } from '../types/workflow.js';
|
|
10
11
|
import type { ConsensusConfig } from '../types/consensus.js';
|
|
11
12
|
import { expandIdea as openaiExpandIdea } from '../adapters/openai.js';
|
|
@@ -52,7 +53,7 @@ export interface PlanModeResult {
|
|
|
52
53
|
*/
|
|
53
54
|
export async function expandIdea(
|
|
54
55
|
idea: string,
|
|
55
|
-
language:
|
|
56
|
+
language: OutputLanguage,
|
|
56
57
|
onProgress?: (message: string) => void
|
|
57
58
|
): Promise<string> {
|
|
58
59
|
onProgress?.('Expanding idea into specification...');
|
|
@@ -75,7 +76,7 @@ export async function expandIdea(
|
|
|
75
76
|
export async function createPlan(
|
|
76
77
|
specification: string,
|
|
77
78
|
context: string = '',
|
|
78
|
-
language:
|
|
79
|
+
language: OutputLanguage = 'python',
|
|
79
80
|
onProgress?: (message: string) => void
|
|
80
81
|
): Promise<string> {
|
|
81
82
|
onProgress?.('Creating development plan...');
|
|
@@ -103,12 +104,45 @@ export async function getProjectContext(
|
|
|
103
104
|
): Promise<string> {
|
|
104
105
|
onProgress?.('Analyzing existing codebase...');
|
|
105
106
|
|
|
106
|
-
// Check if directory has any code
|
|
107
|
+
// Check if directory has any code - check root AND apps/ subdirectories
|
|
107
108
|
try {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const codeExtensions = ['.py', '.ts', '.js', '.tsx', '.jsx'];
|
|
110
|
+
const hasCodeInDir = async (dir: string): Promise<boolean> => {
|
|
111
|
+
try {
|
|
112
|
+
const files = await fs.readdir(dir);
|
|
113
|
+
return files.some((f) => codeExtensions.some((ext) => f.endsWith(ext)));
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let hasCode = await hasCodeInDir(projectDir);
|
|
120
|
+
|
|
121
|
+
// Also check apps/ subdirectories for monorepo/workspace projects
|
|
122
|
+
if (!hasCode) {
|
|
123
|
+
const appsDir = path.join(projectDir, 'apps');
|
|
124
|
+
try {
|
|
125
|
+
const appEntries = await fs.readdir(appsDir, { withFileTypes: true });
|
|
126
|
+
for (const entry of appEntries) {
|
|
127
|
+
if (entry.isDirectory()) {
|
|
128
|
+
const appHasCode = await hasCodeInDir(path.join(appsDir, entry.name));
|
|
129
|
+
if (appHasCode) {
|
|
130
|
+
hasCode = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
// Check one level deeper (apps/frontend/src/)
|
|
134
|
+
const srcDir = path.join(appsDir, entry.name, 'src');
|
|
135
|
+
const srcHasCode = await hasCodeInDir(srcDir);
|
|
136
|
+
if (srcHasCode) {
|
|
137
|
+
hasCode = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// No apps/ directory
|
|
144
|
+
}
|
|
145
|
+
}
|
|
112
146
|
|
|
113
147
|
if (!hasCode) {
|
|
114
148
|
onProgress?.('No existing code found');
|
|
@@ -927,7 +961,7 @@ export async function runPlanMode(
|
|
|
927
961
|
});
|
|
928
962
|
|
|
929
963
|
// Validate fullstack plan structure
|
|
930
|
-
if (spec.language
|
|
964
|
+
if (isWorkspace(spec.language)) {
|
|
931
965
|
onProgress?.('create-plan', 'Validating fullstack plan structure...');
|
|
932
966
|
const validation = validateFullstackPlan(plan);
|
|
933
967
|
|
|
@@ -958,7 +992,7 @@ export async function runPlanMode(
|
|
|
958
992
|
{
|
|
959
993
|
projectDir,
|
|
960
994
|
config: consensusConfig,
|
|
961
|
-
isFullstack: spec.language
|
|
995
|
+
isFullstack: isWorkspace(spec.language),
|
|
962
996
|
language: spec.language,
|
|
963
997
|
onIteration: (iteration, result) => {
|
|
964
998
|
onProgress?.(
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan parsing utilities for task tagging and validation
|
|
3
|
+
* Parses [FE], [BE], [WEB], [INT] tags from plan content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OutputLanguage } from '../types/project.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Task app tags for workspace projects
|
|
10
|
+
*/
|
|
11
|
+
export type TaskAppTag = 'FE' | 'BE' | 'WEB' | 'INT';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* App target derived from task tag
|
|
15
|
+
*/
|
|
16
|
+
export type AppTarget = 'frontend' | 'backend' | 'website' | 'unified';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parsed task with app context
|
|
20
|
+
*/
|
|
21
|
+
export interface ParsedTask {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
appTag?: TaskAppTag;
|
|
25
|
+
appTarget?: AppTarget;
|
|
26
|
+
files?: string[];
|
|
27
|
+
dependencies?: string[];
|
|
28
|
+
acceptanceCriteria?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse task tag from task name
|
|
33
|
+
*
|
|
34
|
+
* @param taskName - Task name potentially containing [FE], [BE], [WEB], [INT]
|
|
35
|
+
* @returns The parsed tag or undefined
|
|
36
|
+
*/
|
|
37
|
+
export function parseTaskTag(taskName: string): TaskAppTag | undefined {
|
|
38
|
+
const tagMatch = taskName.match(/\[(FE|BE|WEB|INT)\]/i);
|
|
39
|
+
if (tagMatch) {
|
|
40
|
+
return tagMatch[1].toUpperCase() as TaskAppTag;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert task tag to app target
|
|
47
|
+
*
|
|
48
|
+
* @param tag - The task tag
|
|
49
|
+
* @returns The app target
|
|
50
|
+
*/
|
|
51
|
+
export function tagToAppTarget(tag: TaskAppTag): AppTarget {
|
|
52
|
+
const mapping: Record<TaskAppTag, AppTarget> = {
|
|
53
|
+
FE: 'frontend',
|
|
54
|
+
BE: 'backend',
|
|
55
|
+
WEB: 'website',
|
|
56
|
+
INT: 'unified',
|
|
57
|
+
};
|
|
58
|
+
return mapping[tag];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validation issues for a task
|
|
63
|
+
*/
|
|
64
|
+
export interface TaskValidationResult {
|
|
65
|
+
valid: boolean;
|
|
66
|
+
issues: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate a task has proper app targeting for workspace projects
|
|
71
|
+
*
|
|
72
|
+
* @param task - The parsed task
|
|
73
|
+
* @param hasWebsite - Whether the project includes a website app
|
|
74
|
+
* @returns Validation result with issues
|
|
75
|
+
*/
|
|
76
|
+
export function validateWorkspaceTask(
|
|
77
|
+
task: ParsedTask,
|
|
78
|
+
hasWebsite: boolean = false
|
|
79
|
+
): TaskValidationResult {
|
|
80
|
+
const issues: string[] = [];
|
|
81
|
+
|
|
82
|
+
const validTags = hasWebsite
|
|
83
|
+
? '[FE], [BE], [WEB], or [INT]'
|
|
84
|
+
: '[FE], [BE], or [INT]';
|
|
85
|
+
|
|
86
|
+
if (!task.appTag) {
|
|
87
|
+
issues.push(`Task "${task.name}" missing ${validTags} tag`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate consistency between tag and appTarget
|
|
91
|
+
if (task.appTag && task.appTarget) {
|
|
92
|
+
const expectedTarget = tagToAppTarget(task.appTag);
|
|
93
|
+
if (task.appTarget !== expectedTarget) {
|
|
94
|
+
issues.push(
|
|
95
|
+
`Task "${task.name}" has [${task.appTag}] tag but App: is "${task.appTarget}" (expected "${expectedTarget}")`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate file paths match app
|
|
101
|
+
if (task.files && task.appTag) {
|
|
102
|
+
const pathValidation: Record<TaskAppTag, { pattern: RegExp; expected: string }> = {
|
|
103
|
+
FE: { pattern: /\/frontend\//, expected: 'apps/frontend/' },
|
|
104
|
+
BE: { pattern: /\/backend\//, expected: 'apps/backend/' },
|
|
105
|
+
WEB: { pattern: /\/website\//, expected: 'apps/website/' },
|
|
106
|
+
INT: { pattern: /.*/, expected: 'any (unified)' },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const { pattern, expected } = pathValidation[task.appTag];
|
|
110
|
+
|
|
111
|
+
// Only validate FE/BE/WEB, not INT
|
|
112
|
+
if (task.appTag !== 'INT') {
|
|
113
|
+
const invalidFiles = task.files.filter((f) => !pattern.test(f));
|
|
114
|
+
if (invalidFiles.length > 0) {
|
|
115
|
+
issues.push(
|
|
116
|
+
`[${task.appTag}] task has files outside ${expected}: ${invalidFiles.join(', ')}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
valid: issues.length === 0,
|
|
124
|
+
issues,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse a task block from plan content
|
|
130
|
+
*
|
|
131
|
+
* @param taskBlock - The raw task block text
|
|
132
|
+
* @returns Parsed task
|
|
133
|
+
*/
|
|
134
|
+
export function parseTaskBlock(taskBlock: string): ParsedTask {
|
|
135
|
+
const lines = taskBlock.split('\n');
|
|
136
|
+
|
|
137
|
+
// Extract task name from first line (### Task X.X [TAG]: Name)
|
|
138
|
+
const titleMatch = lines[0].match(/#{1,4}\s*(?:Task\s+\d+(?:\.\d+)?)?[:\s]*(.+)/i);
|
|
139
|
+
const name = titleMatch ? titleMatch[1].trim() : lines[0].trim();
|
|
140
|
+
|
|
141
|
+
const task: ParsedTask = {
|
|
142
|
+
name,
|
|
143
|
+
appTag: parseTaskTag(name),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Parse App field
|
|
147
|
+
const appMatch = taskBlock.match(/\*\*App\*\*:\s*(\w+)/i);
|
|
148
|
+
if (appMatch) {
|
|
149
|
+
task.appTarget = appMatch[1].toLowerCase() as AppTarget;
|
|
150
|
+
} else if (task.appTag) {
|
|
151
|
+
// Derive from tag if App field not found
|
|
152
|
+
task.appTarget = tagToAppTarget(task.appTag);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Parse Files field
|
|
156
|
+
const filesMatch = taskBlock.match(/\*\*Files\*\*:([\s\S]*?)(?=\*\*|$)/i);
|
|
157
|
+
if (filesMatch) {
|
|
158
|
+
const filesContent = filesMatch[1];
|
|
159
|
+
const fileMatches = filesContent.match(/`([^`]+)`/g);
|
|
160
|
+
if (fileMatches) {
|
|
161
|
+
task.files = fileMatches.map((f) => f.replace(/`/g, ''));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Parse Dependencies
|
|
166
|
+
const depsMatch = taskBlock.match(/\*\*Dependencies\*\*:\s*(.+)/i);
|
|
167
|
+
if (depsMatch && depsMatch[1].trim().toLowerCase() !== 'none') {
|
|
168
|
+
task.dependencies = depsMatch[1]
|
|
169
|
+
.split(',')
|
|
170
|
+
.map((d) => d.trim())
|
|
171
|
+
.filter((d) => d);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Parse Acceptance Criteria
|
|
175
|
+
const criteriaMatch = taskBlock.match(/\*\*Acceptance Criteria\*\*:([\s\S]*?)(?=###|##|$)/i);
|
|
176
|
+
if (criteriaMatch) {
|
|
177
|
+
const criteriaContent = criteriaMatch[1];
|
|
178
|
+
const criteriaItems = criteriaContent.match(/[-*[\]]\s*(.+)/g);
|
|
179
|
+
if (criteriaItems) {
|
|
180
|
+
task.acceptanceCriteria = criteriaItems.map((c) =>
|
|
181
|
+
c.replace(/^[-*[\]x\s]+/i, '').trim()
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return task;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Extract all tasks from a plan
|
|
191
|
+
*
|
|
192
|
+
* @param planContent - The full plan markdown content
|
|
193
|
+
* @returns Array of parsed tasks
|
|
194
|
+
*/
|
|
195
|
+
export function extractTasksFromPlan(planContent: string): ParsedTask[] {
|
|
196
|
+
const tasks: ParsedTask[] = [];
|
|
197
|
+
|
|
198
|
+
// Match task blocks (### Task X.X or #### Task X.X)
|
|
199
|
+
const taskBlockRegex = /#{3,4}\s*(?:Task\s+)?[\d.]+[^#]*?(?=#{2,4}|$)/gi;
|
|
200
|
+
const matches = planContent.matchAll(taskBlockRegex);
|
|
201
|
+
|
|
202
|
+
for (const match of matches) {
|
|
203
|
+
if (match[0].trim()) {
|
|
204
|
+
tasks.push(parseTaskBlock(match[0]));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return tasks;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate all tasks in a plan for workspace projects
|
|
213
|
+
*
|
|
214
|
+
* @param planContent - The full plan markdown content
|
|
215
|
+
* @param language - The project language
|
|
216
|
+
* @returns Validation results
|
|
217
|
+
*/
|
|
218
|
+
export function validatePlanTasks(
|
|
219
|
+
planContent: string,
|
|
220
|
+
language: OutputLanguage
|
|
221
|
+
): {
|
|
222
|
+
valid: boolean;
|
|
223
|
+
tasks: ParsedTask[];
|
|
224
|
+
issues: string[];
|
|
225
|
+
warnings: string[];
|
|
226
|
+
} {
|
|
227
|
+
const isWorkspaceProject = ['fullstack', 'website', 'all'].includes(language);
|
|
228
|
+
const hasWebsite = ['website', 'all'].includes(language);
|
|
229
|
+
|
|
230
|
+
const tasks = extractTasksFromPlan(planContent);
|
|
231
|
+
const issues: string[] = [];
|
|
232
|
+
const warnings: string[] = [];
|
|
233
|
+
|
|
234
|
+
if (!isWorkspaceProject) {
|
|
235
|
+
// Non-workspace projects don't require tagging
|
|
236
|
+
return {
|
|
237
|
+
valid: true,
|
|
238
|
+
tasks,
|
|
239
|
+
issues: [],
|
|
240
|
+
warnings: [],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate each task
|
|
245
|
+
for (const task of tasks) {
|
|
246
|
+
const validation = validateWorkspaceTask(task, hasWebsite);
|
|
247
|
+
if (!validation.valid) {
|
|
248
|
+
// For now, treat as warnings rather than hard errors
|
|
249
|
+
warnings.push(...validation.issues);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for minimum coverage
|
|
254
|
+
if (language === 'fullstack' || language === 'all') {
|
|
255
|
+
const feTasks = tasks.filter((t) => t.appTag === 'FE');
|
|
256
|
+
const beTasks = tasks.filter((t) => t.appTag === 'BE');
|
|
257
|
+
const intTasks = tasks.filter((t) => t.appTag === 'INT');
|
|
258
|
+
|
|
259
|
+
if (feTasks.length === 0) {
|
|
260
|
+
warnings.push('No [FE] frontend tasks found in plan');
|
|
261
|
+
}
|
|
262
|
+
if (beTasks.length === 0) {
|
|
263
|
+
warnings.push('No [BE] backend tasks found in plan');
|
|
264
|
+
}
|
|
265
|
+
if (intTasks.length === 0) {
|
|
266
|
+
warnings.push('No [INT] integration tasks found in plan');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (language === 'all') {
|
|
271
|
+
const webTasks = tasks.filter((t) => t.appTag === 'WEB');
|
|
272
|
+
if (webTasks.length === 0) {
|
|
273
|
+
warnings.push('No [WEB] website tasks found in plan');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
valid: issues.length === 0,
|
|
279
|
+
tasks,
|
|
280
|
+
issues,
|
|
281
|
+
warnings,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get app-specific tasks from a parsed plan
|
|
287
|
+
*
|
|
288
|
+
* @param tasks - Array of parsed tasks
|
|
289
|
+
* @param appTarget - The app target to filter by
|
|
290
|
+
* @returns Filtered tasks
|
|
291
|
+
*/
|
|
292
|
+
export function getTasksByApp(tasks: ParsedTask[], appTarget: AppTarget): ParsedTask[] {
|
|
293
|
+
return tasks.filter((t) => t.appTarget === appTarget);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get task counts by app from a parsed plan
|
|
298
|
+
*
|
|
299
|
+
* @param tasks - Array of parsed tasks
|
|
300
|
+
* @returns Count of tasks per app
|
|
301
|
+
*/
|
|
302
|
+
export function getTaskCountsByApp(tasks: ParsedTask[]): Record<AppTarget, number> {
|
|
303
|
+
const counts: Record<AppTarget, number> = {
|
|
304
|
+
frontend: 0,
|
|
305
|
+
backend: 0,
|
|
306
|
+
website: 0,
|
|
307
|
+
unified: 0,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
for (const task of tasks) {
|
|
311
|
+
if (task.appTarget) {
|
|
312
|
+
counts[task.appTarget]++;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return counts;
|
|
317
|
+
}
|