Perform code optimization and document cleanup (#1)
## Summary - add repository-wide quality tooling and verification scaffolding, including CI workflows, pnpm workspace setup, ESLint/Prettier/markdown checks, and generated-output verification helpers - reorganize skill sources and generation flow by introducing canonical `_source` variants, generator/manifests, reusable helper abstractions, and shared web-automation/browser utilities - clean up and expand documentation so the root README flows into docs and skill docs, with clearer development, reviewer, installer, and workflow guidance ## Notable changes - docs flow and consistency cleanup across `README.md`, `docs/README.md`, and related docs - new scripts for `check`, docs verification, generated-file verification, shell portability, and safe directory replacement - refactors in Atlassian and web-automation skill runtimes to reduce duplication and centralize reusable code - changelog, development documentation, and CI surface updates ## Test Plan - [ ] `pnpm run check` - [ ] review generated/manifests and skill sync outputs - [ ] smoke-check docs flow from `README.md` to `docs/README.md` to skill docs ## Notes - this branch currently includes tracked `skills/web-automation/shared/node_modules` content that should be reviewed carefully as potentially noisy/accidental committed artifacts Co-authored-by: Stefano Fiorini <stefano.fiorini@firsthorizon.com> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* verify-docs-flow.mjs — documentation reading-flow and consistency verifier (M2, S-206)
|
||||
*
|
||||
* Asserts:
|
||||
* (i) every docs/*.md file is linked from docs/README.md
|
||||
* (ii) the reviewer CLI matrix is consistent across the four canonical sources
|
||||
* (iii) the Telegram agent list matches the set of agents with helpers on disk
|
||||
* (iv) all repository-relative paths referenced in README.md and docs/ exist
|
||||
*
|
||||
* Exits 0 when all checks pass; exits 1 with a report when any check fails.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/verify-docs-flow.mjs
|
||||
* pnpm run verify:docs (wired via package.json)
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const REPO_ROOT = path.resolve(__dirname, "..");
|
||||
|
||||
// ── (i) docs/README.md coverage ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Check that every *.md file under docs/ is linked from docs/README.md.
|
||||
* Returns an array of error strings (empty = pass).
|
||||
*
|
||||
* @param {string} repoRoot
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function checkDocsIndexCoverage(repoRoot) {
|
||||
const docsDir = path.join(repoRoot, "docs");
|
||||
const readmePath = path.join(docsDir, "README.md");
|
||||
|
||||
if (!fs.existsSync(readmePath)) {
|
||||
return ["docs/README.md is missing"];
|
||||
}
|
||||
|
||||
const readmeContent = fs.readFileSync(readmePath, "utf8");
|
||||
const errors = [];
|
||||
|
||||
// Collect all *.md files under docs/ except README.md itself
|
||||
const mdFiles = fs
|
||||
.readdirSync(docsDir)
|
||||
.filter((f) => f.endsWith(".md") && f !== "README.md");
|
||||
|
||||
for (const file of mdFiles) {
|
||||
// Accept both ./FILE.md and FILE.md link forms
|
||||
const linked =
|
||||
readmeContent.includes(`](${file})`) ||
|
||||
readmeContent.includes(`](./${file})`);
|
||||
if (!linked) {
|
||||
errors.push(`docs/README.md does not link to docs/${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ── (ii) Reviewer matrix consistency ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Canonical reviewer CLI names expected in workflow docs.
|
||||
* Order matters for the consistency check.
|
||||
*/
|
||||
const EXPECTED_REVIEWER_CLIS = ["codex", "claude", "cursor", "opencode", "pi"];
|
||||
|
||||
/**
|
||||
* Check that the reviewer CLI matrix is consistent across the four canonical
|
||||
* workflow docs. Returns an array of error strings (empty = pass).
|
||||
*
|
||||
* @param {string} repoRoot
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function checkReviewerMatrixConsistency(repoRoot) {
|
||||
const canonical = [
|
||||
"docs/CREATE-PLAN.md",
|
||||
"docs/IMPLEMENT-PLAN.md",
|
||||
"docs/DO-TASK.md",
|
||||
"docs/REVIEWERS.md",
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
|
||||
for (const docPath of canonical) {
|
||||
const fullPath = path.join(repoRoot, docPath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
errors.push(`${docPath} is missing`);
|
||||
continue;
|
||||
}
|
||||
const content = fs.readFileSync(fullPath, "utf8");
|
||||
// Each canonical doc must reference all five CLIs
|
||||
for (const cli of EXPECTED_REVIEWER_CLIS) {
|
||||
if (!content.includes(`\`${cli}\``)) {
|
||||
errors.push(`${docPath}: does not mention reviewer CLI \`${cli}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ── (iii) Telegram agent coverage ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Agents that must have helpers on disk in skills/reviewer-runtime/.
|
||||
* Non-Pi agents use the top-level directory; Pi uses the pi/ sub-directory.
|
||||
*/
|
||||
const TELEGRAM_AGENTS = [
|
||||
{
|
||||
id: "codex",
|
||||
helperPath: "skills/reviewer-runtime/notify-telegram.sh",
|
||||
telegramSectionKeyword: "Codex",
|
||||
},
|
||||
{
|
||||
id: "claude-code",
|
||||
helperPath: "skills/reviewer-runtime/notify-telegram.sh",
|
||||
telegramSectionKeyword: "Claude Code",
|
||||
},
|
||||
{
|
||||
id: "opencode",
|
||||
helperPath: "skills/reviewer-runtime/notify-telegram.sh",
|
||||
telegramSectionKeyword: "OpenCode",
|
||||
},
|
||||
{
|
||||
id: "cursor",
|
||||
helperPath: "skills/reviewer-runtime/notify-telegram.sh",
|
||||
telegramSectionKeyword: "Cursor",
|
||||
},
|
||||
{
|
||||
id: "pi",
|
||||
helperPath: "skills/reviewer-runtime/pi/notify-telegram.sh",
|
||||
telegramSectionKeyword: "Pi",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Check that docs/TELEGRAM-NOTIFICATIONS.md documents every agent that has a
|
||||
* notify-telegram.sh helper on disk. Returns an array of error strings.
|
||||
*
|
||||
* @param {string} repoRoot
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function checkTelegramAgentCoverage(repoRoot) {
|
||||
const telegramDoc = path.join(repoRoot, "docs", "TELEGRAM-NOTIFICATIONS.md");
|
||||
const errors = [];
|
||||
|
||||
if (!fs.existsSync(telegramDoc)) {
|
||||
return ["docs/TELEGRAM-NOTIFICATIONS.md is missing"];
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(telegramDoc, "utf8");
|
||||
|
||||
for (const agent of TELEGRAM_AGENTS) {
|
||||
const helperFullPath = path.join(repoRoot, agent.helperPath);
|
||||
if (!fs.existsSync(helperFullPath)) {
|
||||
errors.push(
|
||||
`Telegram agent ${agent.id}: helper ${agent.helperPath} does not exist on disk`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// The Telegram doc must mention this agent somewhere
|
||||
if (!content.includes(agent.telegramSectionKeyword)) {
|
||||
errors.push(
|
||||
`docs/TELEGRAM-NOTIFICATIONS.md does not document agent: ${agent.telegramSectionKeyword}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pi-specific: must link to PI-COMMON-REVIEWER.md
|
||||
if (!content.includes("PI-COMMON-REVIEWER.md")) {
|
||||
errors.push(
|
||||
"docs/TELEGRAM-NOTIFICATIONS.md must link to docs/PI-COMMON-REVIEWER.md"
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ── (iv) Repo-relative path references ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Patterns to skip when checking path references — these are URLs and
|
||||
* non-filesystem references that look like repo paths but aren't.
|
||||
*/
|
||||
const PATH_SKIP_PATTERNS = [
|
||||
/^https?:\/\//, // HTTP URLs
|
||||
/^\/tmp\//, // /tmp paths (runtime artifacts)
|
||||
/^\$\{/, // shell variable expansions
|
||||
/^~\//, // home directory paths
|
||||
/^\.\.?\//, // relative ./ or ../ — checked differently
|
||||
/node_modules/, // node_modules
|
||||
/^[A-Z_]+_[A-Z_]+$/, // environment variable names
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract candidate repository-relative paths from markdown text.
|
||||
* Looks for:
|
||||
* - markdown links: [text](path)
|
||||
* - inline code: `path`
|
||||
* - bare paths starting with known top-level directories
|
||||
*
|
||||
* @param {string} content
|
||||
* @returns {string[]} - candidate relative paths
|
||||
*/
|
||||
function extractRepoPaths(content) {
|
||||
const candidates = new Set();
|
||||
|
||||
// Markdown links: [text](path) where path does not start with http/https
|
||||
const linkRe = /\[(?:[^\]]*)\]\(([^)]+)\)/g;
|
||||
let m;
|
||||
while ((m = linkRe.exec(content)) !== null) {
|
||||
const href = m[1].split("#")[0].trim(); // strip anchors
|
||||
if (!href) continue;
|
||||
if (PATH_SKIP_PATTERNS.some((p) => p.test(href))) continue;
|
||||
// Normalize ./ prefix
|
||||
const normalized = href.startsWith("./") ? href.slice(2) : href;
|
||||
if (normalized.startsWith("../")) continue; // skip upward traversals
|
||||
candidates.add(normalized);
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all repository-relative paths referenced in README.md and docs/
|
||||
* actually exist in the repository. Returns an array of error strings.
|
||||
*
|
||||
* @param {string} repoRoot
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function checkRepoPathsExist(repoRoot) {
|
||||
const errors = [];
|
||||
const filesToCheck = [
|
||||
path.join(repoRoot, "README.md"),
|
||||
...fs
|
||||
.readdirSync(path.join(repoRoot, "docs"))
|
||||
.filter((f) => f.endsWith(".md"))
|
||||
.map((f) => path.join(repoRoot, "docs", f)),
|
||||
];
|
||||
|
||||
for (const filePath of filesToCheck) {
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
const relDoc = path.relative(repoRoot, filePath);
|
||||
const candidates = extractRepoPaths(content);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
// Skip things that obviously aren't repo paths
|
||||
if (!candidate || candidate.length < 3) continue;
|
||||
// Skip paths that look like markdown-only links (heading anchors)
|
||||
if (candidate.startsWith("#")) continue;
|
||||
// For links relative to docs/, resolve relative to docs/
|
||||
let candidatePath;
|
||||
if (relDoc.startsWith("docs/")) {
|
||||
candidatePath = path.join(repoRoot, "docs", candidate);
|
||||
// If not found there, try relative to repo root
|
||||
if (!fs.existsSync(candidatePath)) {
|
||||
candidatePath = path.join(repoRoot, candidate);
|
||||
}
|
||||
} else {
|
||||
candidatePath = path.join(repoRoot, candidate);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(candidatePath)) {
|
||||
errors.push(`${relDoc}: broken repo path reference → ${candidate}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const repoRoot = REPO_ROOT;
|
||||
const allErrors = [];
|
||||
const checks = [
|
||||
["docs/README.md coverage", checkDocsIndexCoverage],
|
||||
["reviewer matrix consistency", checkReviewerMatrixConsistency],
|
||||
["Telegram agent coverage", checkTelegramAgentCoverage],
|
||||
["repo-relative path existence", checkRepoPathsExist],
|
||||
];
|
||||
|
||||
for (const [label, fn] of checks) {
|
||||
const errors = await fn(repoRoot);
|
||||
if (errors.length > 0) {
|
||||
console.error(`\n[FAIL] ${label}:`);
|
||||
for (const e of errors) {
|
||||
console.error(` - ${e}`);
|
||||
}
|
||||
allErrors.push(...errors);
|
||||
} else {
|
||||
console.log(`[pass] ${label}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
console.error(`\ndocs-flow: ${allErrors.length} error(s) found.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\ndocs-flow: all checks passed.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run main only when executed directly (not when imported by tests)
|
||||
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
|
||||
if (isMain) {
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user