Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/services/readiness/criteria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/services/readiness/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export async function runReadinessReport(options: ReadinessOptions): Promise<Rea
effort: criterion.effort,
status: result.status,
reason: result.reason,
evidence: result.evidence
evidence: result.evidence,
docUrl: criterion.docUrl
});
continue;
}
Expand All @@ -136,7 +137,8 @@ export async function runReadinessReport(options: ReadinessOptions): Promise<Rea
impact: criterion.impact,
effort: criterion.effort,
status: "skip",
reason: "Run with --per-area for area breakdown."
reason: "Run with --per-area for area breakdown.",
docUrl: criterion.docUrl
});
continue;
}
Expand All @@ -158,7 +160,8 @@ export async function runReadinessReport(options: ReadinessOptions): Promise<Rea
impact: criterion.impact,
effort: criterion.effort,
status: "skip",
reason: "No application packages detected."
reason: "No application packages detected.",
docUrl: criterion.docUrl
});
continue;
}
Expand All @@ -181,6 +184,7 @@ export async function runReadinessReport(options: ReadinessOptions): Promise<Rea
effort: criterion.effort,
status,
reason: status === "pass" ? undefined : `Only ${passed}/${total} apps pass this check.`,
docUrl: criterion.docUrl,
passRate,
appSummary: { passed, total },
appFailures: failures
Expand Down Expand Up @@ -217,7 +221,8 @@ export async function runReadinessReport(options: ReadinessOptions): Promise<Rea
effort: criterion.effort,
status: result.status,
reason: result.reason,
evidence: result.evidence
evidence: result.evidence,
docUrl: criterion.docUrl
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/services/readiness/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type ReadinessCriterionResult = {
status: ReadinessStatus;
reason?: string;
evidence?: string[];
docUrl?: string;
passRate?: number;
appSummary?: { passed: number; total: number };
appFailures?: string[];
Expand Down Expand Up @@ -134,6 +135,7 @@ export type ReadinessCriterion = {
scope: ReadinessScope;
impact: "high" | "medium" | "low";
effort: "low" | "medium" | "high";
docUrl?: string;
check: (context: ReadinessContext, app?: RepoApp, area?: Area) => Promise<CheckResult>;
};

Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/services/visualReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -561,7 +563,7 @@ export function generateVisualReport(options: VisualReportOptions): string {
.map(
(c) => `
<div class="criterion-row">
<span>${escapeHtml(c.title)}</span>
<span>${escapeHtml(c.title)}${c.status === "fail" && c.docUrl ? ` <a class="doc-link" href="${escapeHtml(c.docUrl)}" target="_blank" rel="noopener noreferrer">docs</a>` : ""}</span>
<span class="criterion-status ${c.status}">${c.status === "pass" ? "Pass" : c.status === "fail" ? "Fail" : "Skip"}</span>
</div>
`
Expand Down Expand Up @@ -749,6 +751,8 @@ function buildFixFirstHtml(reports: Array<{ repo: string; report: ReadinessRepor
<span class="fix-badge effort-${c.effort}">${c.effort} effort</span>
${multiRepo ? `<span class="fix-badge impact-low">${repos.length} repo${repos.length > 1 ? "s" : ""}</span>` : ""}
</div>
${c.docUrl ? `<a class="doc-link" href="${escapeHtml(c.docUrl)}" target="_blank" rel="noopener noreferrer">Learn more &rarr;</a>` : ""}
</div>
</div>
</div>
`
Expand Down Expand Up @@ -855,6 +859,7 @@ type AiToolingCriterionSummary = {
status: "pass" | "fail";
evidence: string[];
reason: string;
docUrl?: string;
};

type AiToolingData = {
Expand Down Expand Up @@ -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
});
}
}
Expand Down Expand Up @@ -992,7 +998,7 @@ function buildAiToolingHeroHtml(
? `${c.passCount}/${c.totalRepos} repos`
: "Detected"
: escapeHtml(c.reason)
}</div>
}${c.status !== "pass" && c.docUrl ? ` <a class="doc-link" href="${escapeHtml(c.docUrl)}" target="_blank" rel="noopener noreferrer">Learn more &rarr;</a>` : ""}</div>
</div>
</div>
`
Expand Down
12 changes: 11 additions & 1 deletion webapp/frontend/src/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ function buildFixFirst(report) {
${c.impact ? `<span class="fix-badge impact-${safeClass(c.impact, ALLOWED_IMPACT)}">${esc(c.impact)} impact</span>` : ""}
${c.effort ? `<span class="fix-badge effort-${safeClass(c.effort, ALLOWED_EFFORT)}">${esc(c.effort)} effort</span>` : ""}
</div>
${c.docUrl && isSafeDocUrl(c.docUrl) ? `<a class="doc-link" href="${esc(c.docUrl)}" target="_blank" rel="noopener noreferrer">Learn more →</a>` : ""}
</div>
</div>
`
Expand Down Expand Up @@ -226,7 +227,7 @@ function buildAiToolingHero(report) {
<div class="ai-criterion-icon ${safeClass(c.status, ALLOWED_STATUS)}">${c.status === "pass" ? "✓" : "✗"}</div>
<div class="ai-criterion-text">
<div class="ai-criterion-title">${icon} ${esc(c.title)}</div>
<div class="ai-criterion-reason">${c.status === "pass" ? "Detected" : esc(c.reason || "")}</div>
<div class="ai-criterion-reason">${c.status === "pass" ? "Detected" : esc(c.reason || "")}${c.status !== "pass" && c.docUrl && isSafeDocUrl(c.docUrl) ? ` <a class="doc-link" href="${esc(c.docUrl)}" target="_blank" rel="noopener noreferrer">Learn more →</a>` : ""}</div>
</div>
</div>`;
})
Expand Down Expand Up @@ -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;
}
}
Loading