(repoPath, opts = {})
| 472 | * @param {{ includeGit?: boolean }} [opts] |
| 473 | */ |
| 474 | export async function scanRepo(repoPath, opts = {}) { |
| 475 | const includeGit = opts.includeGit !== false; |
| 476 | if (!repoPath || !(await isDirectory(repoPath))) { |
| 477 | return { ok: false, repoPath: repoPath || null, error: "repo path not available" }; |
| 478 | } |
| 479 | |
| 480 | // Modernization docs. Prefer the namespaced .appmod/ copies; the root-level |
| 481 | // files (plan.md / progress.md / summary.md) use very common filenames, so we |
| 482 | // trust them as workflow state only when the provenance gate below passes. |
| 483 | const [apPlan, apProgress, apSummary, rootPlan, rootProgress, rootSummary] = await Promise.all([ |
| 484 | readText(join(repoPath, ".appmod", "plan.md")), |
| 485 | readText(join(repoPath, ".appmod", "progress.md")), |
| 486 | readText(join(repoPath, ".appmod", "summary.md")), |
| 487 | readText(join(repoPath, "plan.md")), |
| 488 | readText(join(repoPath, "progress.md")), |
| 489 | readText(join(repoPath, "summary.md")), |
| 490 | ]); |
| 491 | |
| 492 | // Structured assessment report (written by the assessment step). Optional, |
| 493 | // and the strongest provenance signal that this repo is running the workflow. |
| 494 | let report = null; |
| 495 | const reportRaw = await readText(join(repoPath, ".appmod", "assessment.json")); |
| 496 | if (reportRaw) { |
| 497 | try { |
| 498 | report = normalizeReport(JSON.parse(reportRaw)); |
| 499 | } catch { |
| 500 | report = null; |
| 501 | } |
| 502 | } |
| 503 | |
| 504 | // Provenance gate. A .appmod/ artifact (assessment.json or a namespaced doc) |
| 505 | // means the modernization workflow is active in this repo, so the root docs |
| 506 | // are ours. Otherwise a root doc is trusted only if it carries the explicit |
| 507 | // <!-- appmod-cockpit --> marker. This stops an unrelated summary.md from |
| 508 | // flipping a foreign repo to "completed", or a stray plan.md from injecting |
| 509 | // fake steps into the progress/ordering/autopilot machinery. |
| 510 | const hasAppmodArtifact = |
| 511 | reportRaw != null || apPlan != null || apProgress != null || apSummary != null; |
| 512 | const trusted = (root) => hasAppmodArtifact || hasMarker(root); |
| 513 | const planMd = apPlan || (trusted(rootPlan) ? rootPlan : null); |
| 514 | const progressMd = apProgress || (trusted(rootProgress) ? rootProgress : null); |
| 515 | const summaryMd = apSummary || (trusted(rootSummary) ? rootSummary : null); |
| 516 | |
| 517 | const assessment = await buildAssessment(repoPath); |
| 518 | const skills = await discoverSkills(repoPath); |
| 519 | |
| 520 | const planSteps = parseSteps(planMd); |
| 521 | const progressSteps = parseSteps(progressMd); |
| 522 | // Tag each step with its canonical phase rank for ordering guidance. |
| 523 | for (const arr of [planSteps, progressSteps]) { |
| 524 | for (const st of arr) st.rank = phaseRankFromName(st.section); |
| 525 | } |
| 526 | // Progress is the source of truth for status; fall back to plan steps. |
| 527 | const steps = progressSteps.length ? progressSteps : planSteps; |
| 528 | const percent = percentDone(steps); |
| 529 | const ordering = computeOrdering(steps); |
| 530 | // Prefer an explicit "Validation"/"gates" section for gate status; fall back |
| 531 | // to scanning all steps when no such section exists. |
no test coverage detected