Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.

community.tsBlame243 lines · 1 contributor
f9e7c01Claude1/**
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
13import { getDefaultBranch, getTree } from "../git/repository";
14import type { GitTreeEntry } from "../git/repository";
15
16export type ChecklistKey =
17 | "description"
18 | "readme"
19 | "license"
20 | "code_of_conduct"
21 | "contributing"
22 | "issue_template"
23 | "pr_template"
24 | "topics";
25
26export 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
35export 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
98export type HealthResult = Record<ChecklistKey, boolean>;
99
100export 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
112const README_RE = /^readme(\.(md|txt|rst|markdown))?$/i;
113const LICENSE_RE = /^(license|licence|copying)(\.(md|txt|rst))?$/i;
114const COC_RE = /^code[_-]?of[_-]?conduct(\.(md|txt))?$/i;
115const CONTRIBUTING_RE = /^contributing(\.(md|txt))?$/i;
116const PR_TEMPLATE_RE = /^pull[_-]?request[_-]?template(\.(md|txt))?$/i;
117
118/** Pure name-matcher helpers — exported for unit tests. */
119export function isReadme(name: string): boolean {
120 return README_RE.test(name);
121}
122export function isLicense(name: string): boolean {
123 return LICENSE_RE.test(name);
124}
125export function isCodeOfConduct(name: string): boolean {
126 return COC_RE.test(name);
127}
128export function isContributing(name: string): boolean {
129 return CONTRIBUTING_RE.test(name);
130}
131export 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 */
141export 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. */
170export 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 */
191export 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
230async 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
243export const __internal = { README_RE, LICENSE_RE, COC_RE, CONTRIBUTING_RE, PR_TEMPLATE_RE };