Files
ai-coding-skills/scripts/verify-docs-flow.mjs
T
stefano 251148c3ff
check / check (ubuntu-latest) (push) Successful in 2m5s
check / check (macos-latest) (push) Has been cancelled
check-online / check-online (ubuntu-latest) (push) Successful in 1m53s
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
2026-05-04 04:41:34 +00:00

319 lines
10 KiB
JavaScript

#!/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);
});
}