diff --git a/packages/core/src/services/readiness/criteria.ts b/packages/core/src/services/readiness/criteria.ts index b202a01..682a41e 100644 --- a/packages/core/src/services/readiness/criteria.ts +++ b/packages/core/src/services/readiness/criteria.ts @@ -287,6 +287,7 @@ export function buildCriteria(): ReadinessCriterion[] { scope: "repo", impact: "high", effort: "low", + docUrl: "https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot", check: async (context) => { const rootFound = await hasCustomInstructions(context.repoPath); if (rootFound.length === 0) { @@ -399,6 +400,7 @@ export function buildCriteria(): ReadinessCriterion[] { scope: "repo", impact: "high", effort: "low", + docUrl: "https://code.visualstudio.com/docs/copilot/chat/mcp-servers", check: async (context) => { const found = await hasMcpConfig(context.repoPath); return { @@ -419,6 +421,7 @@ export function buildCriteria(): ReadinessCriterion[] { scope: "repo", impact: "medium", effort: "medium", + docUrl: "https://docs.github.com/en/copilot/customizing-copilot/copilot-extensibility-overview", check: async (context) => { const found = await hasCustomAgents( context.repoPath, @@ -442,6 +445,7 @@ export function buildCriteria(): ReadinessCriterion[] { scope: "repo", impact: "medium", effort: "medium", + docUrl: "https://docs.github.com/en/copilot/customizing-copilot/copilot-extensibility-overview", check: async (context) => { const found = await hasCopilotSkills( context.repoPath, @@ -463,6 +467,7 @@ export function buildCriteria(): ReadinessCriterion[] { scope: "repo", impact: "medium", effort: "low", + docUrl: "https://microsoft.github.io/apm", check: async (context) => { const found = await hasApmConfig(context.repoPath); return { @@ -482,6 +487,7 @@ export function buildCriteria(): ReadinessCriterion[] { scope: "repo", impact: "medium", effort: "low", + docUrl: "https://microsoft.github.io/apm", check: async (context) => { const hasConfig = await hasApmConfig(context.repoPath); if (!hasConfig) { @@ -505,6 +511,7 @@ export function buildCriteria(): ReadinessCriterion[] { scope: "repo", impact: "high", effort: "medium", + docUrl: "https://microsoft.github.io/apm", check: async (context) => { const hasConfig = await hasApmConfig(context.repoPath); if (!hasConfig) { diff --git a/packages/core/src/services/readiness/index.ts b/packages/core/src/services/readiness/index.ts index 9691397..0dfd0c2 100644 --- a/packages/core/src/services/readiness/index.ts +++ b/packages/core/src/services/readiness/index.ts @@ -118,7 +118,8 @@ export async function runReadinessReport(options: ReadinessOptions): Promise Promise; }; diff --git a/packages/core/src/services/visualReport.ts b/packages/core/src/services/visualReport.ts index e7500cd..0f920ee 100644 --- a/packages/core/src/services/visualReport.ts +++ b/packages/core/src/services/visualReport.ts @@ -171,6 +171,8 @@ export function generateVisualReport(options: VisualReportOptions): string { .fix-item-title { font-weight: 600; font-size: 13px; color: var(--color-fg-default); } .fix-item-reason { font-size: 12px; color: var(--color-fg-muted); margin-top: 2px; } .fix-item-badges { display: flex; gap: 6px; margin-top: 4px; } + .doc-link { font-size: 12px; color: var(--color-accent-fg); text-decoration: none; } + .doc-link:hover { text-decoration: underline; } .fix-badge { font-size: 11px; padding: 1px 8px; @@ -561,7 +563,7 @@ export function generateVisualReport(options: VisualReportOptions): string { .map( (c) => `
- ${escapeHtml(c.title)} + ${escapeHtml(c.title)}${c.status === "fail" && c.docUrl ? ` docs` : ""} ${c.status === "pass" ? "Pass" : c.status === "fail" ? "Fail" : "Skip"}
` @@ -749,6 +751,8 @@ function buildFixFirstHtml(reports: Array<{ repo: string; report: ReadinessRepor ${c.effort} effort ${multiRepo ? `${repos.length} repo${repos.length > 1 ? "s" : ""}` : ""} + ${c.docUrl ? `Learn more →` : ""} + ` @@ -855,6 +859,7 @@ type AiToolingCriterionSummary = { status: "pass" | "fail"; evidence: string[]; reason: string; + docUrl?: string; }; type AiToolingData = { @@ -885,7 +890,8 @@ function calculateAiToolingData( totalRepos: 1, status: c.status === "pass" ? "pass" : "fail", evidence: c.evidence ? [...c.evidence] : [], - reason: c.reason || "" + reason: c.reason || "", + docUrl: c.docUrl }); } } @@ -992,7 +998,7 @@ function buildAiToolingHeroHtml( ? `${c.passCount}/${c.totalRepos} repos` : "Detected" : escapeHtml(c.reason) - } + }${c.status !== "pass" && c.docUrl ? ` Learn more →` : ""} ` diff --git a/webapp/frontend/src/report.js b/webapp/frontend/src/report.js index 0537737..84b8b55 100644 --- a/webapp/frontend/src/report.js +++ b/webapp/frontend/src/report.js @@ -174,6 +174,7 @@ function buildFixFirst(report) { ${c.impact ? `${esc(c.impact)} impact` : ""} ${c.effort ? `${esc(c.effort)} effort` : ""} + ${c.docUrl && isSafeDocUrl(c.docUrl) ? `Learn more →` : ""} ` @@ -226,7 +227,7 @@ function buildAiToolingHero(report) {
${c.status === "pass" ? "✓" : "✗"}
${icon} ${esc(c.title)}
-
${c.status === "pass" ? "Detected" : esc(c.reason || "")}
+
${c.status === "pass" ? "Detected" : esc(c.reason || "")}${c.status !== "pass" && c.docUrl && isSafeDocUrl(c.docUrl) ? ` Learn more →` : ""}
`; }) @@ -588,3 +589,12 @@ function isGitHubUrl(url) { return false; } } + +function isSafeDocUrl(url) { + try { + const parsed = new URL(url); + return parsed.protocol === "https:"; + } catch { + return false; + } +}