feat(M1): Baseline verification, quality tooling foundation, and current-state report
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* run-check.mjs — aggregate quality check runner (M1, S-106)
|
||||
*
|
||||
* Runs every quality gate in sequence and reports a summary.
|
||||
* All steps run even if earlier steps fail, so you get a complete
|
||||
* picture of the repository health in one pass.
|
||||
*
|
||||
* Transitional contract (M1):
|
||||
* This script may exit non-zero. Pre-existing failures are recorded in
|
||||
* docs/CLEANUP-BASELINE.md. Only issues introduced by new changes (not
|
||||
* listed in the baseline) constitute a regression.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/lib/run-check.mjs # full check
|
||||
* pnpm run check # same, via pnpm
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, "../..");
|
||||
|
||||
// ── Steps ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const STEPS = [
|
||||
{ label: "lint", cmd: "pnpm", args: ["run", "lint"] },
|
||||
{ label: "typecheck", cmd: "pnpm", args: ["run", "typecheck"] },
|
||||
{ label: "test", cmd: "pnpm", args: ["run", "test"] },
|
||||
{ label: "verify:pi", cmd: "pnpm", args: ["run", "verify:pi"] },
|
||||
{ label: "verify:reviewers", cmd: "pnpm", args: ["run", "verify:reviewers"] },
|
||||
{ label: "verify:docs", cmd: "pnpm", args: ["run", "verify:docs"] },
|
||||
{ label: "verify:generated", cmd: "pnpm", args: ["run", "verify:generated"] },
|
||||
];
|
||||
|
||||
// ── Runner ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
const GREEN = "\x1b[32m";
|
||||
const RED = "\x1b[31m";
|
||||
const BOLD = "\x1b[1m";
|
||||
const DIM = "\x1b[2m";
|
||||
|
||||
function colorize(color, text) {
|
||||
// Respect NO_COLOR env variable
|
||||
if (process.env.NO_COLOR || process.env.CI) return text;
|
||||
return `${color}${text}${RESET}`;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const step of STEPS) {
|
||||
process.stdout.write(`\n${colorize(BOLD, `=== ${step.label} ===`)}\n`);
|
||||
const result = spawnSync(step.cmd, step.args, {
|
||||
cwd: REPO_ROOT,
|
||||
stdio: "inherit",
|
||||
encoding: "utf8",
|
||||
shell: false,
|
||||
});
|
||||
const ok = result.status === 0 && !result.error;
|
||||
results.push({ label: step.label, ok, status: result.status ?? -1 });
|
||||
}
|
||||
|
||||
// ── Summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
process.stdout.write(`\n${colorize(BOLD, "=== check summary ===")}\n`);
|
||||
|
||||
const failures = [];
|
||||
for (const r of results) {
|
||||
if (r.ok) {
|
||||
process.stdout.write(
|
||||
` ${colorize(GREEN, "PASS")} ${r.label}\n`
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
` ${colorize(RED, "FAIL")} ${r.label} ${colorize(DIM, `(exit ${r.status})`)} — see docs/CLEANUP-BASELINE.md if pre-existing\n`
|
||||
);
|
||||
failures.push(r.label);
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write("\n");
|
||||
|
||||
if (failures.length === 0) {
|
||||
process.stdout.write(colorize(GREEN, "All checks passed.\n"));
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
colorize(
|
||||
RED,
|
||||
`${failures.length} check(s) failed: ${failures.join(", ")}\n`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* run-link-check.mjs — markdown link-check runner (M1, S-104)
|
||||
*
|
||||
* Runs markdown-link-check across README.md, docs/, and every SKILL.md
|
||||
* (excluding node_modules and generated agent-variant directories).
|
||||
*
|
||||
* Modes:
|
||||
* --offline (default) — checks only repo-relative links and #anchor links.
|
||||
* All http/https links are ignored. Safe for CI and local dev
|
||||
* without network access.
|
||||
* --online — checks all links, including external URLs, with timeouts
|
||||
* and retries as configured in markdown-link-check.online.json.
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — all checked links are alive (or ignored in offline mode)
|
||||
* 1 — one or more broken links found
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readdirSync, existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, "../..");
|
||||
|
||||
// ── CLI arguments ──────────────────────────────────────────────────────────
|
||||
|
||||
const online = process.argv.includes("--online");
|
||||
const configFile = online
|
||||
? path.join(REPO_ROOT, "markdown-link-check.online.json")
|
||||
: path.join(REPO_ROOT, "markdown-link-check.json");
|
||||
|
||||
// ── File discovery ─────────────────────────────────────────────────────────
|
||||
|
||||
const SKIP_PATHS = new Set([
|
||||
"skills/atlassian/codex",
|
||||
"skills/atlassian/claude-code",
|
||||
"skills/atlassian/cursor",
|
||||
"skills/atlassian/opencode",
|
||||
"skills/atlassian/pi",
|
||||
"skills/web-automation/claude-code",
|
||||
"skills/web-automation/cursor",
|
||||
"skills/web-automation/opencode",
|
||||
"skills/web-automation/pi",
|
||||
"pi-package",
|
||||
]);
|
||||
|
||||
function shouldSkip(absPath) {
|
||||
const rel = path.relative(REPO_ROOT, absPath);
|
||||
for (const skip of SKIP_PATHS) {
|
||||
if (rel === skip || rel.startsWith(skip + path.sep)) return true;
|
||||
}
|
||||
const parts = rel.split(path.sep);
|
||||
return parts.includes("node_modules");
|
||||
}
|
||||
|
||||
function collectMarkdownFiles(dir) {
|
||||
const found = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return found;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (shouldSkip(full)) continue;
|
||||
if (entry.isDirectory()) {
|
||||
found.push(...collectMarkdownFiles(full));
|
||||
} else if (
|
||||
entry.isFile() &&
|
||||
(entry.name.endsWith(".md") || entry.name === "SKILL.md")
|
||||
) {
|
||||
found.push(full);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// ── Collect target files ───────────────────────────────────────────────────
|
||||
|
||||
const files = [
|
||||
path.join(REPO_ROOT, "README.md"),
|
||||
...collectMarkdownFiles(path.join(REPO_ROOT, "docs")),
|
||||
...collectMarkdownFiles(path.join(REPO_ROOT, "skills")),
|
||||
].filter(existsSync);
|
||||
|
||||
// De-duplicate (README.md could appear twice)
|
||||
const uniqueFiles = [...new Set(files)];
|
||||
|
||||
if (uniqueFiles.length === 0) {
|
||||
console.log("link-check: no markdown files found — nothing to check.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`link-check: checking ${uniqueFiles.length} file(s) ` +
|
||||
`[mode: ${online ? "online" : "offline"}]…`
|
||||
);
|
||||
|
||||
// ── Run markdown-link-check ────────────────────────────────────────────────
|
||||
|
||||
const mlcBin = path.join(
|
||||
REPO_ROOT,
|
||||
"node_modules",
|
||||
".bin",
|
||||
"markdown-link-check"
|
||||
);
|
||||
|
||||
let failures = 0;
|
||||
for (const file of uniqueFiles.sort()) {
|
||||
const result = spawnSync(
|
||||
mlcBin,
|
||||
["--config", configFile, "--quiet", file],
|
||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
||||
);
|
||||
const output = (result.stdout + result.stderr).trim();
|
||||
const rel = path.relative(REPO_ROOT, file);
|
||||
if (result.status !== 0) {
|
||||
failures += 1;
|
||||
console.error(`\n--- ${rel} ---`);
|
||||
if (output) console.error(output);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(
|
||||
`\nlink-check: ${failures} file(s) have broken links. ` +
|
||||
`See docs/CLEANUP-BASELINE.md for the as-is baseline.`
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("link-check: all links OK.");
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* run-shellcheck.mjs — shell script quality wrapper (M1, S-105)
|
||||
*
|
||||
* Discovers *.sh files under scripts/ and skills/ (excluding node_modules
|
||||
* and generated agent-variant directories), then runs shellcheck on each.
|
||||
*
|
||||
* shellcheck is a REQUIRED prerequisite. The script fails immediately when
|
||||
* shellcheck is not found on PATH. Install it with:
|
||||
* macOS: brew install shellcheck
|
||||
* Debian: apt-get install shellcheck
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — all files passed shellcheck
|
||||
* 1 — one or more files have shellcheck findings
|
||||
* 2 — shellcheck is missing from PATH
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, "../..");
|
||||
|
||||
// ── Prerequisites check ────────────────────────────────────────────────────
|
||||
|
||||
function checkShellcheck() {
|
||||
const result = spawnSync("shellcheck", ["--version"], { encoding: "utf8" });
|
||||
if (result.error || result.status === null || result.status > 1) {
|
||||
console.error(
|
||||
[
|
||||
"ERROR: shellcheck is not available on PATH.",
|
||||
"",
|
||||
"shellcheck is a required prerequisite for this repository.",
|
||||
"Install it before running lint:",
|
||||
"",
|
||||
" macOS: brew install shellcheck",
|
||||
" Debian/Ubuntu: sudo apt-get install shellcheck",
|
||||
" Other: https://github.com/koalaman/shellcheck#installing",
|
||||
].join("\n")
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// ── File discovery ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Directories to scan for *.sh files.
|
||||
* Relative paths from REPO_ROOT.
|
||||
*/
|
||||
const SCAN_DIRS = ["scripts", "skills"];
|
||||
|
||||
/**
|
||||
* Path segments that indicate a directory should be skipped entirely.
|
||||
* Matches generated agent-variant bundles and npm/pnpm install artifacts.
|
||||
*/
|
||||
const SKIP_SEGMENTS = new Set([
|
||||
"node_modules",
|
||||
// Generated agent-variant directories (excluded per M1 workspace policy)
|
||||
// skills/<skill>/codex — except web-automation/codex which is canonical
|
||||
// We skip all codex variants to be safe; shellcheck only cares about .sh files
|
||||
// and all variants share the same scripts anyway.
|
||||
]);
|
||||
|
||||
/**
|
||||
* Exact relative paths (from REPO_ROOT) to skip, matched after normalisation.
|
||||
* These are the generated variants that duplicate canonical source.
|
||||
*/
|
||||
const SKIP_PATHS = new Set([
|
||||
"skills/atlassian/codex",
|
||||
"skills/atlassian/claude-code",
|
||||
"skills/atlassian/cursor",
|
||||
"skills/atlassian/opencode",
|
||||
"skills/atlassian/pi",
|
||||
"skills/web-automation/claude-code",
|
||||
"skills/web-automation/cursor",
|
||||
"skills/web-automation/opencode",
|
||||
"skills/web-automation/pi",
|
||||
"pi-package",
|
||||
]);
|
||||
|
||||
function shouldSkip(absPath) {
|
||||
const rel = path.relative(REPO_ROOT, absPath);
|
||||
// Check exact-prefix matches (directory and its children)
|
||||
for (const skip of SKIP_PATHS) {
|
||||
if (rel === skip || rel.startsWith(skip + path.sep)) return true;
|
||||
}
|
||||
// Check path-segment matches (e.g. node_modules anywhere in path)
|
||||
for (const seg of rel.split(path.sep)) {
|
||||
if (SKIP_SEGMENTS.has(seg)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function collectShellFiles(dir) {
|
||||
const found = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return found; // directory does not exist — skip silently
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (shouldSkip(full)) continue;
|
||||
if (entry.isDirectory()) {
|
||||
found.push(...collectShellFiles(full));
|
||||
} else if (entry.isFile() && entry.name.endsWith(".sh")) {
|
||||
found.push(full);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
checkShellcheck();
|
||||
|
||||
const files = SCAN_DIRS.flatMap((d) =>
|
||||
collectShellFiles(path.join(REPO_ROOT, d))
|
||||
);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log("shellcheck: no .sh files found — nothing to check.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`shellcheck: scanning ${files.length} file(s)…`);
|
||||
|
||||
let failures = 0;
|
||||
for (const file of files.sort()) {
|
||||
const result = spawnSync("shellcheck", [file], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const output = (result.stdout + result.stderr).trim();
|
||||
const rel = path.relative(REPO_ROOT, file);
|
||||
if (result.status !== 0) {
|
||||
failures += 1;
|
||||
// Print findings prefixed with the relative path so output is reproducible
|
||||
// regardless of cwd.
|
||||
console.error(`\n--- ${rel} ---`);
|
||||
if (output) console.error(output);
|
||||
} else {
|
||||
// Quiet on success — only show problems
|
||||
}
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(
|
||||
`\nshellcheck: ${failures} file(s) have findings. ` +
|
||||
`See docs/CLEANUP-BASELINE.md for the as-is baseline.`
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("shellcheck: all files passed.");
|
||||
process.exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user