CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
community.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| f9e7c01 | 1 | /** |
| 2 | * Block J12 — Community profile / health scorecard. | |
| 3 | * | |
| 4 | * Pure + git-layer helpers that score a repo on its community health files | |
| 5 | * (README, LICENSE, CODE_OF_CONDUCT, CONTRIBUTING, issue templates, PR | |
| 6 | * template) plus repo metadata (description, topics). Mirrors GitHub's | |
| 7 | * "Community Standards" checklist. | |
| 8 | * | |
| 9 | * The check list itself is a pure table so unit tests can verify each rule | |
| 10 | * without touching git or the DB. | |
| 11 | */ | |
| 12 | ||
| 13 | import { getDefaultBranch, getTree } from "../git/repository"; | |
| 14 | import type { GitTreeEntry } from "../git/repository"; | |
| 15 | ||
| 16 | export type ChecklistKey = | |
| 17 | | "description" | |
| 18 | | "readme" | |
| 19 | | "license" | |
| 20 | | "code_of_conduct" | |
| 21 | | "contributing" | |
| 22 | | "issue_template" | |
| 23 | | "pr_template" | |
| 24 | | "topics"; | |
| 25 | ||
| 26 | export interface ChecklistItem { | |
| 27 | key: ChecklistKey; | |
| 28 | label: string; | |
| 29 | description: string; | |
| 30 | /** Path suggested in the "add this file" UI if it's missing. */ | |
| 31 | suggestedPath?: string; | |
| 32 | required: boolean; | |
| 33 | } | |
| 34 | ||
| 35 | export const CHECKLIST: ChecklistItem[] = [ | |
| 36 | { | |
| 37 | key: "description", | |
| 38 | label: "Description", | |
| 39 | description: "A one-line summary so people know what the repo is for.", | |
| 40 | required: true, | |
| 41 | }, | |
| 42 | { | |
| 43 | key: "readme", | |
| 44 | label: "README", | |
| 45 | description: | |
| 46 | "README tells people what the project is and how to use it. Accepted names: README, README.md, README.txt, README.rst.", | |
| 47 | suggestedPath: "README.md", | |
| 48 | required: true, | |
| 49 | }, | |
| 50 | { | |
| 51 | key: "license", | |
| 52 | label: "License", | |
| 53 | description: | |
| 54 | "Without a license file, the default is 'all rights reserved'. Accepted names: LICENSE, LICENSE.md, LICENSE.txt, COPYING.", | |
| 55 | suggestedPath: "LICENSE", | |
| 56 | required: true, | |
| 57 | }, | |
| 58 | { | |
| 59 | key: "code_of_conduct", | |
| 60 | label: "Code of Conduct", | |
| 61 | description: | |
| 62 | "A Code of Conduct clarifies expectations for behaviour in the community.", | |
| 63 | suggestedPath: "CODE_OF_CONDUCT.md", | |
| 64 | required: false, | |
| 65 | }, | |
| 66 | { | |
| 67 | key: "contributing", | |
| 68 | label: "Contributing guidelines", | |
| 69 | description: "Tells potential contributors how to propose changes.", | |
| 70 | suggestedPath: "CONTRIBUTING.md", | |
| 71 | required: false, | |
| 72 | }, | |
| 73 | { | |
| 74 | key: "issue_template", | |
| 75 | label: "Issue templates", | |
| 76 | description: | |
| 77 | "Templates under .github/ISSUE_TEMPLATE/ help reporters file useful bugs.", | |
| 78 | suggestedPath: ".github/ISSUE_TEMPLATE/bug_report.md", | |
| 79 | required: false, | |
| 80 | }, | |
| 81 | { | |
| 82 | key: "pr_template", | |
| 83 | label: "Pull-request template", | |
| 84 | description: | |
| 85 | "A PR template nudges contributors to summarise their change. Accepted names: .github/pull_request_template.md, PULL_REQUEST_TEMPLATE.md.", | |
| 86 | suggestedPath: ".github/pull_request_template.md", | |
| 87 | required: false, | |
| 88 | }, | |
| 89 | { | |
| 90 | key: "topics", | |
| 91 | label: "Topics", | |
| 92 | description: | |
| 93 | "Add topics so people can discover your project on the Explore page.", | |
| 94 | required: false, | |
| 95 | }, | |
| 96 | ]; | |
| 97 | ||
| 98 | export type HealthResult = Record<ChecklistKey, boolean>; | |
| 99 | ||
| 100 | export interface HealthReport { | |
| 101 | items: Array<ChecklistItem & { present: boolean }>; | |
| 102 | passed: number; | |
| 103 | total: number; | |
| 104 | requiredPassed: number; | |
| 105 | requiredTotal: number; | |
| 106 | /** 0 – 100 integer percentage of items passed overall. */ | |
| 107 | score: number; | |
| 108 | /** true when all `required` items are present. */ | |
| 109 | meetsRequired: boolean; | |
| 110 | } | |
| 111 | ||
| 112 | const README_RE = /^readme(\.(md|txt|rst|markdown))?$/i; | |
| 113 | const LICENSE_RE = /^(license|licence|copying)(\.(md|txt|rst))?$/i; | |
| 114 | const COC_RE = /^code[_-]?of[_-]?conduct(\.(md|txt))?$/i; | |
| 115 | const CONTRIBUTING_RE = /^contributing(\.(md|txt))?$/i; | |
| 116 | const PR_TEMPLATE_RE = /^pull[_-]?request[_-]?template(\.(md|txt))?$/i; | |
| 117 | ||
| 118 | /** Pure name-matcher helpers — exported for unit tests. */ | |
| 119 | export function isReadme(name: string): boolean { | |
| 120 | return README_RE.test(name); | |
| 121 | } | |
| 122 | export function isLicense(name: string): boolean { | |
| 123 | return LICENSE_RE.test(name); | |
| 124 | } | |
| 125 | export function isCodeOfConduct(name: string): boolean { | |
| 126 | return COC_RE.test(name); | |
| 127 | } | |
| 128 | export function isContributing(name: string): boolean { | |
| 129 | return CONTRIBUTING_RE.test(name); | |
| 130 | } | |
| 131 | export function isPrTemplate(name: string): boolean { | |
| 132 | return PR_TEMPLATE_RE.test(name); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Given a set of pure inputs (what files exist at the root, what files | |
| 137 | * exist in `.github/`, whether the issue-template directory exists, and the | |
| 138 | * repo metadata), compute the checklist result. No IO — unit tests drive | |
| 139 | * this directly. | |
| 140 | */ | |
| 141 | export function checklistFromInputs(opts: { | |
| 142 | rootEntries: string[]; | |
| 143 | githubEntries: string[]; | |
| 144 | issueTemplateDirExists: boolean; | |
| 145 | description: string | null | undefined; | |
| 146 | topics: string[]; | |
| 147 | }): HealthResult { | |
| 148 | const root = opts.rootEntries.map((n) => n); | |
| 149 | const gh = opts.githubEntries.map((n) => n); | |
| 150 | ||
| 151 | const anywhere = (pred: (n: string) => boolean) => | |
| 152 | root.some(pred) || gh.some(pred); | |
| 153 | ||
| 154 | return { | |
| 155 | description: !!(opts.description && opts.description.trim().length > 0), | |
| 156 | readme: anywhere(isReadme), | |
| 157 | license: anywhere(isLicense), | |
| 158 | code_of_conduct: anywhere(isCodeOfConduct), | |
| 159 | contributing: anywhere(isContributing), | |
| 160 | issue_template: | |
| 161 | opts.issueTemplateDirExists || | |
| 162 | // Fallback: single `ISSUE_TEMPLATE.md` at root or .github | |
| 163 | anywhere((n) => /^issue[_-]?template(\.(md|txt))?$/i.test(n)), | |
| 164 | pr_template: anywhere(isPrTemplate), | |
| 165 | topics: opts.topics.length > 0, | |
| 166 | }; | |
| 167 | } | |
| 168 | ||
| 169 | /** Turn raw booleans into a sorted + annotated report. */ | |
| 170 | export function buildReport(result: HealthResult): HealthReport { | |
| 171 | const items = CHECKLIST.map((item) => ({ ...item, present: result[item.key] })); | |
| 172 | const passed = items.filter((i) => i.present).length; | |
| 173 | const required = items.filter((i) => i.required); | |
| 174 | const requiredPassed = required.filter((i) => i.present).length; | |
| 175 | const score = items.length === 0 ? 0 : Math.round((passed / items.length) * 100); | |
| 176 | return { | |
| 177 | items, | |
| 178 | passed, | |
| 179 | total: items.length, | |
| 180 | requiredPassed, | |
| 181 | requiredTotal: required.length, | |
| 182 | score, | |
| 183 | meetsRequired: requiredPassed === required.length, | |
| 184 | }; | |
| 185 | } | |
| 186 | ||
| 187 | /** | |
| 188 | * Walk the default branch of a repo and produce the community health | |
| 189 | * report. Returns a safe all-false report on git/DB failure — never throws. | |
| 190 | */ | |
| 191 | export async function computeHealth(opts: { | |
| 192 | owner: string; | |
| 193 | repo: string; | |
| 194 | description: string | null | undefined; | |
| 195 | topics: string[]; | |
| 196 | }): Promise<HealthReport> { | |
| 197 | try { | |
| 198 | const branch = | |
| 199 | (await getDefaultBranch(opts.owner, opts.repo)) || "main"; | |
| 200 | const rootTree = await safeTree(opts.owner, opts.repo, branch, ""); | |
| 201 | const githubTree = await safeTree(opts.owner, opts.repo, branch, ".github"); | |
| 202 | const rootNames = rootTree.map((e) => e.name); | |
| 203 | const githubNames = githubTree.map((e) => e.name); | |
| 204 | const issueTemplateDir = githubTree.some( | |
| 205 | (e) => e.type === "tree" && /^ISSUE_TEMPLATE$/i.test(e.name) | |
| 206 | ); | |
| 207 | const result = checklistFromInputs({ | |
| 208 | rootEntries: rootNames, | |
| 209 | githubEntries: githubNames, | |
| 210 | issueTemplateDirExists: issueTemplateDir, | |
| 211 | description: opts.description, | |
| 212 | topics: opts.topics, | |
| 213 | }); | |
| 214 | return buildReport(result); | |
| 215 | } catch { | |
| 216 | const zero: HealthResult = { | |
| 217 | description: false, | |
| 218 | readme: false, | |
| 219 | license: false, | |
| 220 | code_of_conduct: false, | |
| 221 | contributing: false, | |
| 222 | issue_template: false, | |
| 223 | pr_template: false, | |
| 224 | topics: false, | |
| 225 | }; | |
| 226 | return buildReport(zero); | |
| 227 | } | |
| 228 | } | |
| 229 | ||
| 230 | async function safeTree( | |
| 231 | owner: string, | |
| 232 | repo: string, | |
| 233 | ref: string, | |
| 234 | treePath: string | |
| 235 | ): Promise<GitTreeEntry[]> { | |
| 236 | try { | |
| 237 | return await getTree(owner, repo, ref, treePath); | |
| 238 | } catch { | |
| 239 | return []; | |
| 240 | } | |
| 241 | } | |
| 242 | ||
| 243 | export const __internal = { README_RE, LICENSE_RE, COC_RE, CONTRIBUTING_RE, PR_TEMPLATE_RE }; |