mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-16 13:53:04 +08:00
Add discussion audit gate
This commit is contained in:
committed by
Affaan Mustafa
parent
0b6763463f
commit
6887f2952d
350
scripts/discussion-audit.js
Normal file
350
scripts/discussion-audit.js
Normal file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
DEFAULT_DISCUSSION_FIRST,
|
||||
emptyDiscussionSummary,
|
||||
fetchDiscussionSummary,
|
||||
} = require('./lib/github-discussions');
|
||||
|
||||
const SCHEMA_VERSION = 'ecc.discussion-audit.v1';
|
||||
const DEFAULT_REPOS = Object.freeze([
|
||||
'affaan-m/everything-claude-code',
|
||||
'affaan-m/agentshield',
|
||||
'affaan-m/JARVIS',
|
||||
'ECC-Tools/ECC-Tools',
|
||||
'ECC-Tools/ECC-website',
|
||||
]);
|
||||
|
||||
function usage() {
|
||||
console.log([
|
||||
'Usage: node scripts/discussion-audit.js [options]',
|
||||
'',
|
||||
'Audit GitHub discussions for maintainer touch and accepted-answer gaps.',
|
||||
'',
|
||||
'Options:',
|
||||
' --format <text|json|markdown>',
|
||||
' Output format (default: text)',
|
||||
' --json Alias for --format json',
|
||||
' --markdown Alias for --format markdown',
|
||||
' --write <path> Write json or markdown output to a file',
|
||||
' --repo <owner/repo> GitHub repo to inspect; repeatable',
|
||||
' --first <n> Discussions to sample per repo (default: 100)',
|
||||
' --use-env-github-token Keep GITHUB_TOKEN when invoking gh',
|
||||
' --exit-code Return 2 when the audit is not ready',
|
||||
' --help, -h Show this help',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
function readValue(args, index, flagName) {
|
||||
const value = args[index + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
throw new Error(`${flagName} requires a value`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseIntegerFlag(value, flagName) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
throw new Error(`Invalid ${flagName}: ${value}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const parsed = {
|
||||
exitCode: false,
|
||||
first: DEFAULT_DISCUSSION_FIRST,
|
||||
format: 'text',
|
||||
help: false,
|
||||
repos: [],
|
||||
useEnvGithubToken: false,
|
||||
writePath: null,
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--format') {
|
||||
parsed.format = readValue(args, index, arg).toLowerCase();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--format=')) {
|
||||
parsed.format = arg.slice('--format='.length).toLowerCase();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--json') {
|
||||
parsed.format = 'json';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--markdown') {
|
||||
parsed.format = 'markdown';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--write') {
|
||||
parsed.writePath = path.resolve(readValue(args, index, arg));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--write=')) {
|
||||
parsed.writePath = path.resolve(arg.slice('--write='.length));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--repo') {
|
||||
parsed.repos.push(readValue(args, index, arg));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--repo=')) {
|
||||
parsed.repos.push(arg.slice('--repo='.length));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--first') {
|
||||
parsed.first = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--first=')) {
|
||||
parsed.first = parseIntegerFlag(arg.slice('--first='.length), '--first');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--use-env-github-token') {
|
||||
parsed.useEnvGithubToken = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--exit-code') {
|
||||
parsed.exitCode = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
if (!['text', 'json', 'markdown'].includes(parsed.format)) {
|
||||
throw new Error(`Invalid format: ${parsed.format}. Use text, json, or markdown.`);
|
||||
}
|
||||
|
||||
if (parsed.writePath && parsed.format === 'text') {
|
||||
throw new Error('--write requires --json, --markdown, or --format json|markdown');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function buildReport(options) {
|
||||
const repos = options.repos.length > 0 ? options.repos : DEFAULT_REPOS;
|
||||
const repoReports = repos.map(repo => {
|
||||
try {
|
||||
return {
|
||||
repo,
|
||||
discussions: fetchDiscussionSummary(repo, options),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
repo,
|
||||
error: error.message,
|
||||
discussions: emptyDiscussionSummary(),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const totals = {
|
||||
repos: repoReports.length,
|
||||
totalDiscussions: repoReports.reduce((sum, repo) => sum + repo.discussions.totalCount, 0),
|
||||
sampledDiscussions: repoReports.reduce((sum, repo) => sum + repo.discussions.sampledCount, 0),
|
||||
needingMaintainerTouch: repoReports.reduce((sum, repo) => sum + repo.discussions.needingMaintainerTouch.length, 0),
|
||||
missingAcceptedAnswer: repoReports.reduce((sum, repo) => sum + repo.discussions.answerableWithoutAcceptedAnswer.length, 0),
|
||||
errors: repoReports.filter(repo => repo.error).length,
|
||||
};
|
||||
|
||||
const checks = [
|
||||
{
|
||||
id: 'discussion-fetch',
|
||||
status: totals.errors === 0 ? 'pass' : 'fail',
|
||||
summary: `GitHub discussion fetch errors: ${totals.errors}`,
|
||||
fix: 'Re-run with working gh authentication or ECC_GH_SHIM for deterministic tests.',
|
||||
},
|
||||
{
|
||||
id: 'discussion-maintainer-touch',
|
||||
status: totals.needingMaintainerTouch === 0 ? 'pass' : 'fail',
|
||||
summary: `discussions needing maintainer touch: ${totals.needingMaintainerTouch}`,
|
||||
fix: 'Respond to or route discussions without maintainer touch.',
|
||||
},
|
||||
{
|
||||
id: 'discussion-accepted-answers',
|
||||
status: totals.missingAcceptedAnswer === 0 ? 'pass' : 'fail',
|
||||
summary: `answerable discussions missing accepted answer: ${totals.missingAcceptedAnswer}`,
|
||||
fix: 'Mark an accepted answer or route Q&A discussions that still need resolution.',
|
||||
},
|
||||
];
|
||||
const topActions = checks
|
||||
.filter(check => check.status === 'fail')
|
||||
.map(check => ({
|
||||
id: check.id,
|
||||
summary: check.summary,
|
||||
fix: check.fix,
|
||||
}));
|
||||
|
||||
return {
|
||||
schema_version: SCHEMA_VERSION,
|
||||
generatedAt: new Date().toISOString(),
|
||||
ready: topActions.length === 0,
|
||||
sampleFirst: options.first,
|
||||
repos: repoReports,
|
||||
totals,
|
||||
checks,
|
||||
top_actions: topActions,
|
||||
};
|
||||
}
|
||||
|
||||
function markdownEscape(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/\|/g, '\\|')
|
||||
.replace(/\r?\n/g, '<br>');
|
||||
}
|
||||
|
||||
function renderText(report) {
|
||||
const lines = [
|
||||
`ECC Discussion Audit: ${report.ready ? 'ready' : 'attention required'}`,
|
||||
`Generated: ${report.generatedAt}`,
|
||||
`Repos: ${report.totals.repos}`,
|
||||
`Discussions sampled: ${report.totals.sampledDiscussions}/${report.totals.totalDiscussions}`,
|
||||
`Needs maintainer touch: ${report.totals.needingMaintainerTouch}`,
|
||||
`Missing accepted answers: ${report.totals.missingAcceptedAnswer}`,
|
||||
`Fetch errors: ${report.totals.errors}`,
|
||||
'',
|
||||
'Checks:',
|
||||
];
|
||||
|
||||
for (const check of report.checks) {
|
||||
lines.push(` ${check.status.toUpperCase()} ${check.id}: ${check.summary}`);
|
||||
}
|
||||
|
||||
lines.push('', 'Top actions:');
|
||||
if (report.top_actions.length === 0) {
|
||||
lines.push(' none');
|
||||
} else {
|
||||
for (const action of report.top_actions) {
|
||||
lines.push(` - ${action.id}: ${action.fix}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function renderMarkdown(report) {
|
||||
const lines = [
|
||||
'# ECC Discussion Audit',
|
||||
'',
|
||||
`Generated: ${report.generatedAt}`,
|
||||
`Status: ${report.ready ? 'ready' : 'attention required'}`,
|
||||
'',
|
||||
'## Summary',
|
||||
'',
|
||||
'| Surface | Count | Target | Status |',
|
||||
'| --- | ---: | ---: | --- |',
|
||||
`| Fetch errors | ${report.totals.errors} | 0 | ${report.totals.errors === 0 ? 'PASS' : 'FAIL'} |`,
|
||||
`| Discussions needing maintainer touch | ${report.totals.needingMaintainerTouch} | 0 | ${report.totals.needingMaintainerTouch === 0 ? 'PASS' : 'FAIL'} |`,
|
||||
`| Answerable discussions missing accepted answer | ${report.totals.missingAcceptedAnswer} | 0 | ${report.totals.missingAcceptedAnswer === 0 ? 'PASS' : 'FAIL'} |`,
|
||||
'',
|
||||
'## Repositories',
|
||||
'',
|
||||
'| Repository | Total | Sampled | Needs maintainer | Missing answers |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
|
||||
for (const repo of report.repos) {
|
||||
lines.push(
|
||||
`| \`${markdownEscape(repo.repo)}\` | ${repo.discussions.totalCount} | ${repo.discussions.sampledCount} | ${repo.discussions.needingMaintainerTouch.length} | ${repo.discussions.answerableWithoutAcceptedAnswer.length} |`
|
||||
);
|
||||
}
|
||||
|
||||
lines.push('', '## Top Actions', '');
|
||||
if (report.top_actions.length === 0) {
|
||||
lines.push('- none');
|
||||
} else {
|
||||
for (const action of report.top_actions) {
|
||||
lines.push(`- \`${markdownEscape(action.id)}\`: ${markdownEscape(action.fix)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function writeOutput(writePath, output) {
|
||||
fs.mkdirSync(path.dirname(writePath), { recursive: true });
|
||||
fs.writeFileSync(writePath, output, 'utf8');
|
||||
}
|
||||
|
||||
function renderReport(report, format) {
|
||||
if (format === 'json') {
|
||||
return `${JSON.stringify(report, null, 2)}\n`;
|
||||
}
|
||||
|
||||
if (format === 'markdown') {
|
||||
return renderMarkdown(report);
|
||||
}
|
||||
|
||||
return renderText(report);
|
||||
}
|
||||
|
||||
function main() {
|
||||
let options;
|
||||
try {
|
||||
options = parseArgs(process.argv);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.help) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
const report = buildReport(options);
|
||||
const output = renderReport(report, options.format);
|
||||
|
||||
if (options.writePath) {
|
||||
writeOutput(options.writePath, output);
|
||||
}
|
||||
|
||||
process.stdout.write(output);
|
||||
|
||||
if (options.exitCode && !report.ready) {
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildReport,
|
||||
parseArgs,
|
||||
renderMarkdown,
|
||||
renderReport,
|
||||
renderText,
|
||||
};
|
||||
141
scripts/lib/github-discussions.js
Normal file
141
scripts/lib/github-discussions.js
Normal file
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const DEFAULT_DISCUSSION_FIRST = 100;
|
||||
const MAINTAINER_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
const DISCUSSION_QUERY = 'query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation category { name isAnswerable } answer { url authorAssociation } comments(first: 20) { nodes { authorAssociation } } } } } }';
|
||||
|
||||
function splitRepo(repo) {
|
||||
const [owner, name] = String(repo || '').split('/');
|
||||
if (!owner || !name) {
|
||||
throw new Error(`Invalid repo: ${repo}`);
|
||||
}
|
||||
return { owner, name };
|
||||
}
|
||||
|
||||
function runCommand(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env || process.env,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`${command} ${args.join(' ')} failed: ${result.error.message}`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command} ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
||||
}
|
||||
|
||||
return result.stdout || '';
|
||||
}
|
||||
|
||||
function runGhJson(args, options = {}) {
|
||||
const shimPath = process.env.ECC_GH_SHIM;
|
||||
const command = shimPath ? process.execPath : 'gh';
|
||||
const commandArgs = shimPath ? [shimPath, ...args] : args;
|
||||
const env = { ...process.env };
|
||||
|
||||
if (!options.useEnvGithubToken) {
|
||||
delete env.GITHUB_TOKEN;
|
||||
}
|
||||
|
||||
const stdout = runCommand(command, commandArgs, { env });
|
||||
try {
|
||||
return JSON.parse(stdout || 'null');
|
||||
} catch (error) {
|
||||
throw new Error(`gh ${args.join(' ')} returned invalid JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function discussionNeedsMaintainerTouch(discussion) {
|
||||
if (MAINTAINER_ASSOCIATIONS.has(discussion.authorAssociation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
discussion.answer
|
||||
&& MAINTAINER_ASSOCIATIONS.has(discussion.answer.authorAssociation)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const comments = discussion.comments && Array.isArray(discussion.comments.nodes)
|
||||
? discussion.comments.nodes
|
||||
: [];
|
||||
return !comments.some(comment => MAINTAINER_ASSOCIATIONS.has(comment.authorAssociation));
|
||||
}
|
||||
|
||||
function discussionNeedsAcceptedAnswer(discussion) {
|
||||
return Boolean(
|
||||
discussion
|
||||
&& discussion.category
|
||||
&& discussion.category.isAnswerable
|
||||
&& !discussion.answer
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeDiscussion(discussion) {
|
||||
return {
|
||||
number: discussion.number,
|
||||
title: discussion.title,
|
||||
url: discussion.url,
|
||||
updatedAt: discussion.updatedAt,
|
||||
category: discussion.category ? discussion.category.name : null,
|
||||
};
|
||||
}
|
||||
|
||||
function fetchDiscussionSummary(repo, options = {}) {
|
||||
const { owner, name } = splitRepo(repo);
|
||||
const first = Number.isFinite(options.first) ? options.first : DEFAULT_DISCUSSION_FIRST;
|
||||
const payload = runGhJson([
|
||||
'api',
|
||||
'graphql',
|
||||
'-f',
|
||||
`owner=${owner}`,
|
||||
'-f',
|
||||
`name=${name}`,
|
||||
'-F',
|
||||
`first=${first}`,
|
||||
'-f',
|
||||
`query=${DISCUSSION_QUERY}`,
|
||||
], options);
|
||||
const repository = payload && payload.data && payload.data.repository;
|
||||
const discussions = repository && repository.discussions;
|
||||
const nodes = discussions && Array.isArray(discussions.nodes) ? discussions.nodes : [];
|
||||
const needingTouch = nodes.filter(discussionNeedsMaintainerTouch);
|
||||
const missingAcceptedAnswer = nodes.filter(discussionNeedsAcceptedAnswer);
|
||||
|
||||
return {
|
||||
enabled: Boolean(repository && repository.hasDiscussionsEnabled),
|
||||
totalCount: discussions && Number.isFinite(discussions.totalCount) ? discussions.totalCount : 0,
|
||||
sampledCount: nodes.length,
|
||||
needingMaintainerTouch: needingTouch.map(summarizeDiscussion),
|
||||
answerableWithoutAcceptedAnswer: missingAcceptedAnswer.map(summarizeDiscussion),
|
||||
};
|
||||
}
|
||||
|
||||
function emptyDiscussionSummary() {
|
||||
return {
|
||||
enabled: false,
|
||||
totalCount: 0,
|
||||
sampledCount: 0,
|
||||
needingMaintainerTouch: [],
|
||||
answerableWithoutAcceptedAnswer: [],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_DISCUSSION_FIRST,
|
||||
DISCUSSION_QUERY,
|
||||
MAINTAINER_ASSOCIATIONS,
|
||||
discussionNeedsAcceptedAnswer,
|
||||
discussionNeedsMaintainerTouch,
|
||||
emptyDiscussionSummary,
|
||||
fetchDiscussionSummary,
|
||||
splitRepo,
|
||||
summarizeDiscussion,
|
||||
};
|
||||
@@ -5,6 +5,10 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const {
|
||||
emptyDiscussionSummary,
|
||||
fetchDiscussionSummary,
|
||||
} = require('./lib/github-discussions');
|
||||
|
||||
const SCHEMA_VERSION = 'ecc.platform-audit.v1';
|
||||
const DEFAULT_REPOS = Object.freeze([
|
||||
@@ -19,9 +23,6 @@ const DEFAULT_THRESHOLDS = Object.freeze({
|
||||
maxOpenIssues: 20,
|
||||
maxDirtyFiles: 0,
|
||||
});
|
||||
const MAINTAINER_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
const DISCUSSION_QUERY = 'query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation comments(first: 20) { nodes { authorAssociation } } } } } }';
|
||||
|
||||
function usage() {
|
||||
console.log([
|
||||
'Usage: node scripts/platform-audit.js [options]',
|
||||
@@ -333,57 +334,6 @@ function inspectGit(rootDir, options) {
|
||||
}
|
||||
}
|
||||
|
||||
function discussionNeedsMaintainerTouch(discussion) {
|
||||
if (MAINTAINER_ASSOCIATIONS.has(discussion.authorAssociation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const comments = discussion.comments && Array.isArray(discussion.comments.nodes)
|
||||
? discussion.comments.nodes
|
||||
: [];
|
||||
return !comments.some(comment => MAINTAINER_ASSOCIATIONS.has(comment.authorAssociation));
|
||||
}
|
||||
|
||||
function splitRepo(repo) {
|
||||
const [owner, name] = String(repo || '').split('/');
|
||||
if (!owner || !name) {
|
||||
throw new Error(`Invalid repo: ${repo}`);
|
||||
}
|
||||
return { owner, name };
|
||||
}
|
||||
|
||||
function fetchDiscussionSummary(repo, options) {
|
||||
const { owner, name } = splitRepo(repo);
|
||||
const payload = runGhJson([
|
||||
'api',
|
||||
'graphql',
|
||||
'-f',
|
||||
`owner=${owner}`,
|
||||
'-f',
|
||||
`name=${name}`,
|
||||
'-F',
|
||||
'first=100',
|
||||
'-f',
|
||||
`query=${DISCUSSION_QUERY}`,
|
||||
], options);
|
||||
const repository = payload && payload.data && payload.data.repository;
|
||||
const discussions = repository && repository.discussions;
|
||||
const nodes = discussions && Array.isArray(discussions.nodes) ? discussions.nodes : [];
|
||||
const needingTouch = nodes.filter(discussionNeedsMaintainerTouch);
|
||||
|
||||
return {
|
||||
enabled: Boolean(repository && repository.hasDiscussionsEnabled),
|
||||
totalCount: discussions && Number.isFinite(discussions.totalCount) ? discussions.totalCount : 0,
|
||||
sampledCount: nodes.length,
|
||||
needingMaintainerTouch: needingTouch.map(discussion => ({
|
||||
number: discussion.number,
|
||||
title: discussion.title,
|
||||
url: discussion.url,
|
||||
updatedAt: discussion.updatedAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function fetchGithubRepo(repo, options) {
|
||||
const prs = runGhJson([
|
||||
'pr',
|
||||
@@ -431,6 +381,7 @@ function buildGithubReport(options) {
|
||||
openPrs: 0,
|
||||
openIssues: 0,
|
||||
discussionsNeedingMaintainerTouch: 0,
|
||||
discussionsMissingAcceptedAnswer: 0,
|
||||
dirtyPrs: 0,
|
||||
errors: 0,
|
||||
},
|
||||
@@ -446,12 +397,7 @@ function buildGithubReport(options) {
|
||||
error: error.message,
|
||||
openPrs: 0,
|
||||
openIssues: 0,
|
||||
discussions: {
|
||||
enabled: false,
|
||||
totalCount: 0,
|
||||
sampledCount: 0,
|
||||
needingMaintainerTouch: [],
|
||||
},
|
||||
discussions: emptyDiscussionSummary(),
|
||||
dirtyPrs: [],
|
||||
};
|
||||
}
|
||||
@@ -464,6 +410,7 @@ function buildGithubReport(options) {
|
||||
openPrs: repoReports.reduce((sum, repo) => sum + repo.openPrs, 0),
|
||||
openIssues: repoReports.reduce((sum, repo) => sum + repo.openIssues, 0),
|
||||
discussionsNeedingMaintainerTouch: repoReports.reduce((sum, repo) => sum + repo.discussions.needingMaintainerTouch.length, 0),
|
||||
discussionsMissingAcceptedAnswer: repoReports.reduce((sum, repo) => sum + repo.discussions.answerableWithoutAcceptedAnswer.length, 0),
|
||||
dirtyPrs: repoReports.reduce((sum, repo) => sum + repo.dirtyPrs.length, 0),
|
||||
errors: repoReports.filter(repo => repo.error).length,
|
||||
},
|
||||
@@ -482,9 +429,12 @@ function buildLocalEvidenceChecks(rootDir) {
|
||||
return [
|
||||
buildCheck(
|
||||
'platform-audit-cli-surface',
|
||||
packageScripts['platform:audit'] === 'node scripts/platform-audit.js' ? 'pass' : 'fail',
|
||||
'package.json exposes the platform audit command',
|
||||
{ fix: 'Add "platform:audit": "node scripts/platform-audit.js" to package.json.' }
|
||||
packageScripts['platform:audit'] === 'node scripts/platform-audit.js'
|
||||
&& packageScripts['discussion:audit'] === 'node scripts/discussion-audit.js'
|
||||
? 'pass'
|
||||
: 'fail',
|
||||
'package.json exposes the platform and discussion audit commands',
|
||||
{ fix: 'Add platform:audit and discussion:audit commands to package.json.' }
|
||||
),
|
||||
buildCheck(
|
||||
'roadmap-linear-mirror',
|
||||
@@ -567,6 +517,13 @@ function buildReport(options) {
|
||||
{ fix: 'Respond to or route discussions without maintainer touch before marking the queue current.' }
|
||||
));
|
||||
|
||||
checks.push(buildCheck(
|
||||
'github-discussion-answers',
|
||||
github.totals.discussionsMissingAcceptedAnswer === 0 ? 'pass' : 'fail',
|
||||
`answerable discussions missing accepted answer: ${github.totals.discussionsMissingAcceptedAnswer}`,
|
||||
{ fix: 'Mark an accepted answer or route Q&A discussions that still need resolution.' }
|
||||
));
|
||||
|
||||
checks.push(buildCheck(
|
||||
'github-conflict-queue',
|
||||
github.totals.dirtyPrs === 0 ? 'pass' : 'fail',
|
||||
@@ -611,6 +568,7 @@ function renderText(report) {
|
||||
`Open PRs: ${report.github.totals.openPrs}/${report.thresholds.maxOpenPrs}`,
|
||||
`Open issues: ${report.github.totals.openIssues}/${report.thresholds.maxOpenIssues}`,
|
||||
`Discussions needing maintainer touch: ${report.github.totals.discussionsNeedingMaintainerTouch}`,
|
||||
`Answerable discussions missing accepted answer: ${report.github.totals.discussionsMissingAcceptedAnswer}`,
|
||||
`Conflicting open PRs: ${report.github.totals.dirtyPrs}`,
|
||||
'',
|
||||
'Checks:',
|
||||
@@ -666,18 +624,19 @@ function renderMarkdown(report) {
|
||||
`| Open PRs | ${report.github.totals.openPrs} | ${report.thresholds.maxOpenPrs} | ${report.github.totals.openPrs <= report.thresholds.maxOpenPrs ? 'PASS' : 'FAIL'} |`,
|
||||
`| Open issues | ${report.github.totals.openIssues} | ${report.thresholds.maxOpenIssues} | ${report.github.totals.openIssues <= report.thresholds.maxOpenIssues ? 'PASS' : 'FAIL'} |`,
|
||||
`| Discussions needing maintainer touch | ${report.github.totals.discussionsNeedingMaintainerTouch} | 0 | ${report.github.totals.discussionsNeedingMaintainerTouch === 0 ? 'PASS' : 'FAIL'} |`,
|
||||
`| Answerable discussions missing accepted answer | ${report.github.totals.discussionsMissingAcceptedAnswer} | 0 | ${report.github.totals.discussionsMissingAcceptedAnswer === 0 ? 'PASS' : 'FAIL'} |`,
|
||||
`| Conflicting open PRs | ${report.github.totals.dirtyPrs} | 0 | ${report.github.totals.dirtyPrs === 0 ? 'PASS' : 'FAIL'} |`,
|
||||
`| Blocking dirty files | ${report.git.blockingDirtyCount} | ${report.thresholds.maxDirtyFiles} | ${report.git.blockingDirtyCount <= report.thresholds.maxDirtyFiles ? 'PASS' : 'FAIL'} |`,
|
||||
'',
|
||||
'## Repositories',
|
||||
'',
|
||||
'| Repository | PRs | Issues | Discussions sampled | Needs maintainer | Dirty PRs |',
|
||||
'| --- | ---: | ---: | ---: | ---: | ---: |',
|
||||
'| Repository | PRs | Issues | Discussions sampled | Needs maintainer | Missing answers | Dirty PRs |',
|
||||
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
|
||||
for (const repo of report.github.repos) {
|
||||
lines.push(
|
||||
`| \`${markdownEscape(repo.repo)}\` | ${repo.openPrs || 0} | ${repo.openIssues || 0} | ${repo.discussions ? repo.discussions.sampledCount : 0} | ${repo.discussions ? repo.discussions.needingMaintainerTouch.length : 0} | ${repo.dirtyPrs ? repo.dirtyPrs.length : 0} |`
|
||||
`| \`${markdownEscape(repo.repo)}\` | ${repo.openPrs || 0} | ${repo.openIssues || 0} | ${repo.discussions ? repo.discussions.sampledCount : 0} | ${repo.discussions ? repo.discussions.needingMaintainerTouch.length : 0} | ${repo.discussions ? repo.discussions.answerableWithoutAcceptedAnswer.length : 0} | ${repo.dirtyPrs ? repo.dirtyPrs.length : 0} |`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user