(projectRoot)
| 257 | }; |
| 258 | } |
| 259 | |
| 260 | function readCampaigns(projectRoot) { |
| 261 | const campaignDir = path.join(projectRoot, '.planning', 'campaigns'); |
| 262 | const files = listFiles(campaignDir, (entry) => entry.endsWith('.md')); |
| 263 | const campaigns = []; |
| 264 | const skipped = []; |
| 265 | |
| 266 | for (const filePath of files) { |
| 267 | const content = readText(filePath); |
| 268 | if (!content) continue; |
| 269 | try { |
| 270 | const slug = path.basename(filePath, '.md'); |
| 271 | const parsed = parseCampaignContent(content, { slug }); |
| 272 | const direction = extractDirection(content); |
| 273 | let status = normalizeStatus(parsed); |
| 274 | const packagePhase = packagePhaseReadiness(parsed); |
| 275 | const reviewPackageEvidence = reviewPackageEvidenceStatus(content, projectRoot); |
| 276 | if ( |
| 277 | reviewPackageEvidence && |
| 278 | !reviewPackageEvidence.pass && |
| 279 | (packagePhase.readyForPackage || status === 'needs-completion') |
| 280 | ) { |
| 281 | status = 'needs-review-package'; |
| 282 | } |
| 283 | if (status === 'completed' && path.dirname(filePath) === campaignDir) { |
| 284 | status = 'needs-archive'; |
| 285 | } |
| 286 | campaigns.push({ |
| 287 | slug, |
| 288 | filePath, |
| 289 | status, |
| 290 | direction, |
| 291 | phase: phaseSummary(parsed), |
| 292 | packagePhase, |
| 293 | reviewPackageEvidence, |
| 294 | lastDecision: lastDecision(content), |
| 295 | modifiedAt: fs.statSync(filePath).mtime.toISOString(), |
| 296 | }); |
| 297 | } catch (error) { |
| 298 | skipped.push({ filePath, reason: error.message }); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | campaigns.sort((left, right) => { |
| 303 | const rank = (status) => { |
| 304 | if (status === 'active') return 0; |
| 305 | if (status === 'needs-review-package') return 1; |
| 306 | if (status === 'needs-completion') return 1; |
| 307 | if (status === 'needs-archive') return 1; |
| 308 | if (status.includes('approval') || status.includes('pending') || status === 'needs-continue') return 1; |
| 309 | if (status === 'parked' || status === 'paused') return 2; |
| 310 | return 3; |
| 311 | }; |
| 312 | return rank(left.status) - rank(right.status) || |
| 313 | new Date(right.modifiedAt).getTime() - new Date(left.modifiedAt).getTime(); |
| 314 | }); |
| 315 | |
| 316 | return { campaigns, skipped }; |
no test coverage detected