specweave 0.21.3 → 0.22.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.
Files changed (59) hide show
  1. package/CLAUDE.md +198 -6
  2. package/README.md +33 -3
  3. package/dist/plugins/specweave-github/lib/CodeValidator.d.ts +101 -0
  4. package/dist/plugins/specweave-github/lib/CodeValidator.d.ts.map +1 -0
  5. package/dist/plugins/specweave-github/lib/CodeValidator.js +219 -0
  6. package/dist/plugins/specweave-github/lib/CodeValidator.js.map +1 -0
  7. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts +182 -0
  8. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts.map +1 -0
  9. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js +603 -0
  10. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js.map +1 -0
  11. package/dist/plugins/specweave-github/lib/types.d.ts +34 -0
  12. package/dist/plugins/specweave-github/lib/types.d.ts.map +1 -1
  13. package/dist/src/cli/commands/init.d.ts.map +1 -1
  14. package/dist/src/cli/commands/init.js +60 -5
  15. package/dist/src/cli/commands/init.js.map +1 -1
  16. package/dist/src/config/types.d.ts +8 -8
  17. package/dist/src/core/living-docs/CompletionPropagator.d.ts.map +1 -1
  18. package/dist/src/core/living-docs/CompletionPropagator.js +4 -3
  19. package/dist/src/core/living-docs/CompletionPropagator.js.map +1 -1
  20. package/dist/src/core/living-docs/SpecDistributor.d.ts +5 -0
  21. package/dist/src/core/living-docs/SpecDistributor.d.ts.map +1 -1
  22. package/dist/src/core/living-docs/SpecDistributor.js +12 -0
  23. package/dist/src/core/living-docs/SpecDistributor.js.map +1 -1
  24. package/dist/src/core/living-docs/project-detector.d.ts.map +1 -1
  25. package/dist/src/core/living-docs/project-detector.js +38 -0
  26. package/dist/src/core/living-docs/project-detector.js.map +1 -1
  27. package/dist/src/core/types/config.d.ts +23 -0
  28. package/dist/src/core/types/config.d.ts.map +1 -1
  29. package/dist/src/core/types/config.js +10 -0
  30. package/dist/src/core/types/config.js.map +1 -1
  31. package/dist/src/init/ArchitecturePresenter.d.ts +47 -0
  32. package/dist/src/init/ArchitecturePresenter.d.ts.map +1 -0
  33. package/dist/src/init/ArchitecturePresenter.js +180 -0
  34. package/dist/src/init/ArchitecturePresenter.js.map +1 -0
  35. package/dist/src/init/InitFlow.d.ts.map +1 -1
  36. package/dist/src/init/InitFlow.js +30 -1
  37. package/dist/src/init/InitFlow.js.map +1 -1
  38. package/dist/src/init/architecture/CostEstimator.d.ts +52 -0
  39. package/dist/src/init/architecture/CostEstimator.d.ts.map +1 -0
  40. package/dist/src/init/architecture/CostEstimator.js +107 -0
  41. package/dist/src/init/architecture/CostEstimator.js.map +1 -0
  42. package/dist/src/init/architecture/InfrastructureMapper.d.ts +41 -0
  43. package/dist/src/init/architecture/InfrastructureMapper.d.ts.map +1 -0
  44. package/dist/src/init/architecture/InfrastructureMapper.js +140 -0
  45. package/dist/src/init/architecture/InfrastructureMapper.js.map +1 -0
  46. package/dist/src/init/architecture/ProjectGenerator.d.ts +44 -0
  47. package/dist/src/init/architecture/ProjectGenerator.d.ts.map +1 -0
  48. package/dist/src/init/architecture/ProjectGenerator.js +216 -0
  49. package/dist/src/init/architecture/ProjectGenerator.js.map +1 -0
  50. package/dist/src/init/research/src/config/types.d.ts +8 -8
  51. package/package.json +9 -8
  52. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  53. package/plugins/specweave-github/lib/CodeValidator.js +195 -0
  54. package/plugins/specweave-github/lib/CodeValidator.ts +284 -0
  55. package/plugins/specweave-github/lib/ThreeLayerSyncManager.js +545 -0
  56. package/plugins/specweave-github/lib/ThreeLayerSyncManager.ts +809 -0
  57. package/plugins/specweave-github/lib/types.ts +38 -0
  58. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1200 -0
  59. package/src/templates/AGENTS.md.template +22 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specweave",
3
- "version": "0.21.3",
3
+ "version": "0.22.0",
4
4
  "description": "Spec-driven development framework for Claude Code. AI-native workflow with living documentation, intelligent agents, and multilingual support (9 languages). Enterprise-grade traceability with permanent specs and temporary increments.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,17 +16,18 @@
16
16
  "dev": "tsc --watch",
17
17
  "prepare": "npm run build",
18
18
  "prepublishOnly": "npm run rebuild",
19
- "test:unit": "jest tests/unit --coverage",
20
- "test:integration": "jest tests/integration --coverage",
19
+ "test:unit": "vitest run tests/unit --coverage",
20
+ "test:integration": "vitest run tests/integration --coverage",
21
21
  "test:smoke": "bash tests/smoke/smoke-test.sh",
22
22
  "test:e2e": "playwright test tests/e2e/ --grep-invert=\"(should default to claude adapter|should use claude adapter when explicitly requested|should use generic adapter|should create .claude|should initialize project with specweave init|should create correct directory structure|should handle non-interactive mode correctly|should validate config.json structure|should create .specweave directory structure|should create CLAUDE.md and AGENTS.md|should initialize git repository|should install SpecWeave|should scaffold SaaS|should create proper directory|should create required configuration|should install core skills|should install core agents|should have deployment|should have Stripe|ADO Sync|Increment Discipline Blocking|Self-Reflection|Increment Discipline Enforcement)\"",
23
23
  "test:all": "npm run test:unit && npm run test:integration && npm run test:e2e",
24
- "test:coverage": "jest --coverage --coverageReporters=text --coverageReporters=lcov",
24
+ "test:coverage": "vitest run --coverage",
25
25
  "test": "npm run test:smoke",
26
26
  "benchmark": "ts-node tests/performance/run-all-benchmarks.ts",
27
27
  "validate:plugins": "node scripts/validate-plugin-manifests.cjs",
28
28
  "validate:platforms": "npx ts-node scripts/validate-platforms.ts",
29
29
  "metrics:dora": "node dist/src/metrics/dora-calculator.js",
30
+ "migrate:copy-sync": "node scripts/migrate-to-copy-based-sync.js",
30
31
  "generate:diagrams": "bash scripts/generate-diagram-svgs.sh",
31
32
  "docs:dev": "cd docs-site && npm start",
32
33
  "docs:build": "cd docs-site && npm run build",
@@ -94,15 +95,15 @@
94
95
  "@playwright/test": "^1.48.0",
95
96
  "@types/fs-extra": "^11.0.4",
96
97
  "@types/inquirer": "^9.0.7",
97
- "@types/jest": "^30.0.0",
98
98
  "@types/js-yaml": "^4.0.9",
99
99
  "@types/node": "^24.10.0",
100
+ "@vitest/coverage-v8": "^2.1.0",
101
+ "@vitest/ui": "^2.1.0",
100
102
  "ajv": "^8.17.1",
101
103
  "ajv-formats": "^3.0.1",
102
104
  "dotenv": "^17.2.3",
103
105
  "gray-matter": "^4.0.3",
104
- "jest": "^30.2.0",
105
- "ts-jest": "^29.4.5",
106
- "typescript": "^5.3.0"
106
+ "typescript": "^5.3.0",
107
+ "vitest": "^2.1.0"
107
108
  }
108
109
  }
@@ -0,0 +1,170 @@
1
+ import { AdoClientV2 } from "./ado-client-v2.js";
2
+ import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
3
+ import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
4
+ import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
5
+ import path from "path";
6
+ import fs from "fs/promises";
7
+ async function syncSpecToAdoWithEnhancedContent(options) {
8
+ const { specPath, organization, project, dryRun = false, verbose = false } = options;
9
+ try {
10
+ const baseSpec = await parseSpecContent(specPath);
11
+ if (!baseSpec) {
12
+ return {
13
+ success: false,
14
+ action: "error",
15
+ error: "Failed to parse spec content"
16
+ };
17
+ }
18
+ if (verbose) {
19
+ console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
20
+ }
21
+ const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
22
+ const rootDir = await findSpecWeaveRoot(specPath);
23
+ const mapper = new SpecIncrementMapper(rootDir);
24
+ const mapping = await mapper.mapSpecToIncrements(specId);
25
+ if (verbose) {
26
+ console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
27
+ }
28
+ const taskMapping = buildTaskMapping(mapping.increments, organization, project);
29
+ const architectureDocs = await findArchitectureDocs(rootDir, specId);
30
+ const enhancedSpec = {
31
+ ...baseSpec,
32
+ summary: baseSpec.description,
33
+ taskMapping,
34
+ architectureDocs
35
+ };
36
+ const builder = new EnhancedContentBuilder();
37
+ const description = builder.buildExternalDescription(enhancedSpec);
38
+ if (verbose) {
39
+ console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
40
+ }
41
+ if (dryRun) {
42
+ console.log("\u{1F50D} DRY RUN - Would create/update feature with:");
43
+ console.log(` Title: ${baseSpec.title}`);
44
+ console.log(` Description length: ${description.length}`);
45
+ return {
46
+ success: true,
47
+ action: "no-change",
48
+ tasksLinked: taskMapping?.tasks.length || 0
49
+ };
50
+ }
51
+ if (!organization || !project) {
52
+ return {
53
+ success: false,
54
+ action: "error",
55
+ error: "Azure DevOps organization/project not specified"
56
+ };
57
+ }
58
+ const profile = {
59
+ provider: "ado",
60
+ displayName: `${organization}/${project}`,
61
+ config: {
62
+ organization,
63
+ project
64
+ },
65
+ timeRange: { default: "1M", max: "6M" }
66
+ };
67
+ const pat = process.env.AZURE_DEVOPS_PAT || "";
68
+ const client = new AdoClientV2(profile, pat);
69
+ const existingFeature = await findExistingFeature(client, baseSpec.identifier.compact);
70
+ let result;
71
+ if (existingFeature) {
72
+ await client.updateWorkItem(existingFeature.id, {
73
+ title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
74
+ description
75
+ });
76
+ result = {
77
+ success: true,
78
+ action: "updated",
79
+ featureId: existingFeature.id,
80
+ featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${existingFeature.id}`,
81
+ tasksLinked: taskMapping?.tasks.length || 0
82
+ };
83
+ } else {
84
+ const feature = await client.createEpic({
85
+ title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
86
+ description,
87
+ tags: ["spec", "external-tool-sync"]
88
+ });
89
+ result = {
90
+ success: true,
91
+ action: "created",
92
+ featureId: feature.id,
93
+ featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${feature.id}`,
94
+ tasksLinked: taskMapping?.tasks.length || 0
95
+ };
96
+ }
97
+ if (verbose) {
98
+ console.log(`\u2705 ${result.action === "created" ? "Created" : "Updated"} feature #${result.featureId}`);
99
+ }
100
+ return result;
101
+ } catch (error) {
102
+ return {
103
+ success: false,
104
+ action: "error",
105
+ error: error.message
106
+ };
107
+ }
108
+ }
109
+ async function findSpecWeaveRoot(specPath) {
110
+ let currentDir = path.dirname(specPath);
111
+ while (true) {
112
+ const specweaveDir = path.join(currentDir, ".specweave");
113
+ try {
114
+ await fs.access(specweaveDir);
115
+ return currentDir;
116
+ } catch {
117
+ const parentDir = path.dirname(currentDir);
118
+ if (parentDir === currentDir) {
119
+ throw new Error(".specweave directory not found");
120
+ }
121
+ currentDir = parentDir;
122
+ }
123
+ }
124
+ }
125
+ function buildTaskMapping(increments, organization, project) {
126
+ if (increments.length === 0) return void 0;
127
+ const firstIncrement = increments[0];
128
+ const tasks = firstIncrement.tasks.map((task) => ({
129
+ id: task.id,
130
+ title: task.title,
131
+ userStories: task.userStories
132
+ }));
133
+ return {
134
+ incrementId: firstIncrement.id,
135
+ tasks,
136
+ tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/repo?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
137
+ };
138
+ }
139
+ async function findArchitectureDocs(rootDir, specId) {
140
+ const docs = [];
141
+ const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
142
+ try {
143
+ const adrDir = path.join(archDir, "adr");
144
+ try {
145
+ const adrs = await fs.readdir(adrDir);
146
+ const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
147
+ for (const adr of relatedAdrs) {
148
+ docs.push({
149
+ type: "adr",
150
+ path: path.join(adrDir, adr),
151
+ title: adr.replace(".md", "").replace(/-/g, " ")
152
+ });
153
+ }
154
+ } catch {
155
+ }
156
+ } catch {
157
+ }
158
+ return docs;
159
+ }
160
+ async function findExistingFeature(client, specId) {
161
+ try {
162
+ const features = await client.queryWorkItems(`[System.Title] Contains '[${specId}]' AND [System.WorkItemType] = 'Feature'`);
163
+ return features[0] || null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+ export {
169
+ syncSpecToAdoWithEnhancedContent
170
+ };
@@ -0,0 +1,195 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ class CodeValidator {
4
+ constructor(options = {}) {
5
+ this.options = {
6
+ minLines: options.minLines ?? 3,
7
+ minChars: options.minChars ?? 50,
8
+ projectRoot: options.projectRoot ?? process.cwd()
9
+ };
10
+ }
11
+ /**
12
+ * Validate that code exists for a task
13
+ *
14
+ * Extracts file paths from task description and verifies:
15
+ * 1. Files exist
16
+ * 2. Files have meaningful content
17
+ * 3. Files are not just stubs
18
+ *
19
+ * @param taskDescription - Task description with file paths
20
+ * @param taskId - Task ID for error messages
21
+ * @returns Validation result
22
+ */
23
+ async validateTask(taskDescription, taskId) {
24
+ const filePaths = this.extractFilePaths(taskDescription);
25
+ if (filePaths.length === 0) {
26
+ return {
27
+ taskId,
28
+ valid: true,
29
+ files: [],
30
+ reason: "No file paths specified in task description"
31
+ };
32
+ }
33
+ const fileResults = [];
34
+ let allValid = true;
35
+ const reasons = [];
36
+ for (const filePath of filePaths) {
37
+ const result = await this.validateFile(filePath);
38
+ fileResults.push(result);
39
+ if (!result.exists) {
40
+ allValid = false;
41
+ reasons.push(`File not found: ${filePath}`);
42
+ } else if (!result.hasContent) {
43
+ allValid = false;
44
+ reasons.push(`File has no meaningful content: ${filePath} (${result.reason})`);
45
+ }
46
+ }
47
+ return {
48
+ taskId,
49
+ valid: allValid,
50
+ files: fileResults,
51
+ reason: reasons.length > 0 ? reasons.join("; ") : void 0
52
+ };
53
+ }
54
+ /**
55
+ * Validate a single file
56
+ *
57
+ * @param filePath - Path to file (relative or absolute)
58
+ * @returns File validation result
59
+ */
60
+ async validateFile(filePath) {
61
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(this.options.projectRoot, filePath);
62
+ const exists = await fs.pathExists(absolutePath);
63
+ if (!exists) {
64
+ return {
65
+ path: filePath,
66
+ exists: false,
67
+ hasContent: false,
68
+ lineCount: 0,
69
+ reason: "File does not exist"
70
+ };
71
+ }
72
+ const content = await fs.readFile(absolutePath, "utf-8");
73
+ const lines = content.split("\n");
74
+ const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
75
+ if (nonEmptyLines.length < this.options.minLines) {
76
+ return {
77
+ path: filePath,
78
+ exists: true,
79
+ hasContent: false,
80
+ lineCount: nonEmptyLines.length,
81
+ reason: `Only ${nonEmptyLines.length} non-empty lines (minimum: ${this.options.minLines})`
82
+ };
83
+ }
84
+ const trimmedContent = content.trim();
85
+ if (trimmedContent.length < this.options.minChars) {
86
+ return {
87
+ path: filePath,
88
+ exists: true,
89
+ hasContent: false,
90
+ lineCount: nonEmptyLines.length,
91
+ reason: `Only ${trimmedContent.length} characters (minimum: ${this.options.minChars})`
92
+ };
93
+ }
94
+ const stubPatterns = [
95
+ /^\/\/\s*TODO:/i,
96
+ /^#\s*TODO:/i,
97
+ /^\s*throw new Error\(['"]Not implemented['"]\)/i,
98
+ /^\s*return null;?\s*$/m,
99
+ /^\s*pass\s*$/m,
100
+ // Python
101
+ /^\s*\.\.\.$/m
102
+ // TypeScript
103
+ ];
104
+ const isStub = stubPatterns.some((pattern) => pattern.test(trimmedContent));
105
+ if (isStub) {
106
+ return {
107
+ path: filePath,
108
+ exists: true,
109
+ hasContent: false,
110
+ lineCount: nonEmptyLines.length,
111
+ reason: "File contains stub/placeholder code"
112
+ };
113
+ }
114
+ return {
115
+ path: filePath,
116
+ exists: true,
117
+ hasContent: true,
118
+ lineCount: nonEmptyLines.length
119
+ };
120
+ }
121
+ /**
122
+ * Extract file paths from task description
123
+ *
124
+ * Supports multiple formats:
125
+ * - **Files**: src/foo.ts, src/bar.ts
126
+ * - **Files to create**: src/foo.ts
127
+ * - **Files to modify**: src/bar.ts
128
+ * - Inline code blocks with file paths
129
+ *
130
+ * @param description - Task description text
131
+ * @returns Array of file paths
132
+ */
133
+ extractFilePaths(description) {
134
+ const paths = /* @__PURE__ */ new Set();
135
+ const filesMatch = description.match(/\*\*Files\*\*:\s*([^\n]+)/i);
136
+ if (filesMatch) {
137
+ const filePaths = filesMatch[1].split(",").map((p) => p.trim());
138
+ filePaths.forEach((p) => paths.add(p));
139
+ }
140
+ const createMatch = description.match(/\*\*Files to create\*\*:\s*([^\n]+)/i);
141
+ if (createMatch) {
142
+ const filePaths = createMatch[1].split(",").map((p) => p.trim());
143
+ filePaths.forEach((p) => paths.add(p));
144
+ }
145
+ const modifyMatch = description.match(/\*\*Files to modify\*\*:\s*([^\n]+)/i);
146
+ if (modifyMatch) {
147
+ const filePaths = modifyMatch[1].split(",").map((p) => p.trim());
148
+ filePaths.forEach((p) => paths.add(p));
149
+ }
150
+ const inlineMatches = description.matchAll(/`([a-zA-Z0-9_\-./]+\.(ts|js|tsx|jsx|py|java|go|rs|cpp|c|h))`/g);
151
+ for (const match of inlineMatches) {
152
+ paths.add(match[1]);
153
+ }
154
+ const listMatches = description.matchAll(/^[-*]\s+([a-zA-Z0-9_\-./]+\.(ts|js|tsx|jsx|py|java|go|rs|cpp|c|h))/gm);
155
+ for (const match of listMatches) {
156
+ paths.add(match[1]);
157
+ }
158
+ return Array.from(paths);
159
+ }
160
+ /**
161
+ * Batch validate multiple tasks
162
+ *
163
+ * @param tasks - Array of {taskId, description}
164
+ * @returns Array of validation results
165
+ */
166
+ async validateTasks(tasks) {
167
+ const validationPromises = tasks.map(
168
+ (task) => this.validateTask(task.description, task.taskId)
169
+ );
170
+ return Promise.all(validationPromises);
171
+ }
172
+ /**
173
+ * Get summary of validation results
174
+ *
175
+ * @param results - Array of task validation results
176
+ * @returns Summary statistics
177
+ */
178
+ summarizeResults(results) {
179
+ const total = results.length;
180
+ const valid = results.filter((r) => r.valid).length;
181
+ const invalid = results.filter((r) => !r.valid).length;
182
+ const noFiles = results.filter((r) => r.files.length === 0).length;
183
+ const invalidTasks = results.filter((r) => !r.valid).map((r) => r.taskId);
184
+ return {
185
+ total,
186
+ valid,
187
+ invalid,
188
+ noFiles,
189
+ invalidTasks
190
+ };
191
+ }
192
+ }
193
+ export {
194
+ CodeValidator
195
+ };
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Code Validator
3
+ *
4
+ * Validates that completed tasks have actual code implementation.
5
+ * Prevents marking tasks as complete when:
6
+ * - Files don't exist
7
+ * - Files are empty or have trivial content
8
+ * - Implementation is incomplete
9
+ *
10
+ * Used by ThreeLayerSyncManager to enforce code-completion discipline.
11
+ *
12
+ * @module CodeValidator
13
+ */
14
+
15
+ import fs from 'fs-extra';
16
+ import path from 'path';
17
+
18
+ /**
19
+ * File validation result
20
+ */
21
+ export interface FileValidationResult {
22
+ path: string;
23
+ exists: boolean;
24
+ hasContent: boolean;
25
+ lineCount: number;
26
+ reason?: string; // Why validation failed
27
+ }
28
+
29
+ /**
30
+ * Task validation result
31
+ */
32
+ export interface TaskValidationResult {
33
+ taskId: string;
34
+ valid: boolean;
35
+ files: FileValidationResult[];
36
+ reason?: string; // Summary of why task validation failed
37
+ }
38
+
39
+ /**
40
+ * CodeValidator options
41
+ */
42
+ export interface CodeValidatorOptions {
43
+ minLines?: number; // Minimum lines for a file to be considered non-empty (default: 3)
44
+ minChars?: number; // Minimum characters for meaningful content (default: 50)
45
+ projectRoot?: string; // Project root for resolving relative paths
46
+ }
47
+
48
+ export class CodeValidator {
49
+ private options: Required<CodeValidatorOptions>;
50
+
51
+ constructor(options: CodeValidatorOptions = {}) {
52
+ this.options = {
53
+ minLines: options.minLines ?? 3,
54
+ minChars: options.minChars ?? 50,
55
+ projectRoot: options.projectRoot ?? process.cwd()
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Validate that code exists for a task
61
+ *
62
+ * Extracts file paths from task description and verifies:
63
+ * 1. Files exist
64
+ * 2. Files have meaningful content
65
+ * 3. Files are not just stubs
66
+ *
67
+ * @param taskDescription - Task description with file paths
68
+ * @param taskId - Task ID for error messages
69
+ * @returns Validation result
70
+ */
71
+ async validateTask(taskDescription: string, taskId: string): Promise<TaskValidationResult> {
72
+ const filePaths = this.extractFilePaths(taskDescription);
73
+
74
+ if (filePaths.length === 0) {
75
+ // No file paths specified - consider it valid (task might be non-code)
76
+ return {
77
+ taskId,
78
+ valid: true,
79
+ files: [],
80
+ reason: 'No file paths specified in task description'
81
+ };
82
+ }
83
+
84
+ const fileResults: FileValidationResult[] = [];
85
+ let allValid = true;
86
+ const reasons: string[] = [];
87
+
88
+ for (const filePath of filePaths) {
89
+ const result = await this.validateFile(filePath);
90
+ fileResults.push(result);
91
+
92
+ if (!result.exists) {
93
+ allValid = false;
94
+ reasons.push(`File not found: ${filePath}`);
95
+ } else if (!result.hasContent) {
96
+ allValid = false;
97
+ reasons.push(`File has no meaningful content: ${filePath} (${result.reason})`);
98
+ }
99
+ }
100
+
101
+ return {
102
+ taskId,
103
+ valid: allValid,
104
+ files: fileResults,
105
+ reason: reasons.length > 0 ? reasons.join('; ') : undefined
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Validate a single file
111
+ *
112
+ * @param filePath - Path to file (relative or absolute)
113
+ * @returns File validation result
114
+ */
115
+ async validateFile(filePath: string): Promise<FileValidationResult> {
116
+ // Resolve relative paths
117
+ const absolutePath = path.isAbsolute(filePath)
118
+ ? filePath
119
+ : path.join(this.options.projectRoot, filePath);
120
+
121
+ // Check if file exists
122
+ const exists = await fs.pathExists(absolutePath);
123
+ if (!exists) {
124
+ return {
125
+ path: filePath,
126
+ exists: false,
127
+ hasContent: false,
128
+ lineCount: 0,
129
+ reason: 'File does not exist'
130
+ };
131
+ }
132
+
133
+ // Read file content
134
+ const content = await fs.readFile(absolutePath, 'utf-8');
135
+ const lines = content.split('\n');
136
+ const nonEmptyLines = lines.filter(line => line.trim().length > 0);
137
+
138
+ // Check line count
139
+ if (nonEmptyLines.length < this.options.minLines) {
140
+ return {
141
+ path: filePath,
142
+ exists: true,
143
+ hasContent: false,
144
+ lineCount: nonEmptyLines.length,
145
+ reason: `Only ${nonEmptyLines.length} non-empty lines (minimum: ${this.options.minLines})`
146
+ };
147
+ }
148
+
149
+ // Check character count
150
+ const trimmedContent = content.trim();
151
+ if (trimmedContent.length < this.options.minChars) {
152
+ return {
153
+ path: filePath,
154
+ exists: true,
155
+ hasContent: false,
156
+ lineCount: nonEmptyLines.length,
157
+ reason: `Only ${trimmedContent.length} characters (minimum: ${this.options.minChars})`
158
+ };
159
+ }
160
+
161
+ // Check for stub patterns (common placeholder patterns)
162
+ const stubPatterns = [
163
+ /^\/\/\s*TODO:/i,
164
+ /^#\s*TODO:/i,
165
+ /^\s*throw new Error\(['"]Not implemented['"]\)/i,
166
+ /^\s*return null;?\s*$/m,
167
+ /^\s*pass\s*$/m, // Python
168
+ /^\s*\.\.\.$/m // TypeScript
169
+ ];
170
+
171
+ const isStub = stubPatterns.some(pattern => pattern.test(trimmedContent));
172
+ if (isStub) {
173
+ return {
174
+ path: filePath,
175
+ exists: true,
176
+ hasContent: false,
177
+ lineCount: nonEmptyLines.length,
178
+ reason: 'File contains stub/placeholder code'
179
+ };
180
+ }
181
+
182
+ // All checks passed
183
+ return {
184
+ path: filePath,
185
+ exists: true,
186
+ hasContent: true,
187
+ lineCount: nonEmptyLines.length
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Extract file paths from task description
193
+ *
194
+ * Supports multiple formats:
195
+ * - **Files**: src/foo.ts, src/bar.ts
196
+ * - **Files to create**: src/foo.ts
197
+ * - **Files to modify**: src/bar.ts
198
+ * - Inline code blocks with file paths
199
+ *
200
+ * @param description - Task description text
201
+ * @returns Array of file paths
202
+ */
203
+ extractFilePaths(description: string): string[] {
204
+ const paths: Set<string> = new Set();
205
+
206
+ // Pattern 1: **Files**: path1, path2, path3
207
+ const filesMatch = description.match(/\*\*Files\*\*:\s*([^\n]+)/i);
208
+ if (filesMatch) {
209
+ const filePaths = filesMatch[1].split(',').map(p => p.trim());
210
+ filePaths.forEach(p => paths.add(p));
211
+ }
212
+
213
+ // Pattern 2: **Files to create**: path1, path2
214
+ const createMatch = description.match(/\*\*Files to create\*\*:\s*([^\n]+)/i);
215
+ if (createMatch) {
216
+ const filePaths = createMatch[1].split(',').map(p => p.trim());
217
+ filePaths.forEach(p => paths.add(p));
218
+ }
219
+
220
+ // Pattern 3: **Files to modify**: path1, path2
221
+ const modifyMatch = description.match(/\*\*Files to modify\*\*:\s*([^\n]+)/i);
222
+ if (modifyMatch) {
223
+ const filePaths = modifyMatch[1].split(',').map(p => p.trim());
224
+ filePaths.forEach(p => paths.add(p));
225
+ }
226
+
227
+ // Pattern 4: Inline file references (e.g., `src/foo/bar.ts`)
228
+ const inlineMatches = description.matchAll(/`([a-zA-Z0-9_\-./]+\.(ts|js|tsx|jsx|py|java|go|rs|cpp|c|h))`/g);
229
+ for (const match of inlineMatches) {
230
+ paths.add(match[1]);
231
+ }
232
+
233
+ // Pattern 5: Markdown list items with file paths
234
+ const listMatches = description.matchAll(/^[-*]\s+([a-zA-Z0-9_\-./]+\.(ts|js|tsx|jsx|py|java|go|rs|cpp|c|h))/gm);
235
+ for (const match of listMatches) {
236
+ paths.add(match[1]);
237
+ }
238
+
239
+ return Array.from(paths);
240
+ }
241
+
242
+ /**
243
+ * Batch validate multiple tasks
244
+ *
245
+ * @param tasks - Array of {taskId, description}
246
+ * @returns Array of validation results
247
+ */
248
+ async validateTasks(tasks: Array<{ taskId: string; description: string }>): Promise<TaskValidationResult[]> {
249
+ // Use parallel validation for performance
250
+ const validationPromises = tasks.map(task =>
251
+ this.validateTask(task.description, task.taskId)
252
+ );
253
+
254
+ return Promise.all(validationPromises);
255
+ }
256
+
257
+ /**
258
+ * Get summary of validation results
259
+ *
260
+ * @param results - Array of task validation results
261
+ * @returns Summary statistics
262
+ */
263
+ summarizeResults(results: TaskValidationResult[]): {
264
+ total: number;
265
+ valid: number;
266
+ invalid: number;
267
+ noFiles: number;
268
+ invalidTasks: string[];
269
+ } {
270
+ const total = results.length;
271
+ const valid = results.filter(r => r.valid).length;
272
+ const invalid = results.filter(r => !r.valid).length;
273
+ const noFiles = results.filter(r => r.files.length === 0).length;
274
+ const invalidTasks = results.filter(r => !r.valid).map(r => r.taskId);
275
+
276
+ return {
277
+ total,
278
+ valid,
279
+ invalid,
280
+ noFiles,
281
+ invalidTasks
282
+ };
283
+ }
284
+ }