📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-08-24 09:30:03 +08:00
parent 14e7209e78
commit 5d88510274
153 changed files with 3428 additions and 1203 deletions
+3 -1
View File
@@ -101,7 +101,9 @@ try {
process.exit(1);
}
const report = message.content[0]?.text ?? "";
// The first block is not always the text one — an endpoint that returns a
// thinking block first would otherwise yield undefined.
const report = message.content.find((block) => block.type === "text")?.text ?? "";
const scoreMatch = report.match(/Health\s+Score[:\s]+(\d+)/i);
const score = scoreMatch ? parseInt(scoreMatch[1], 10) : null;
+241
View File
@@ -0,0 +1,241 @@
// Generates the README star-history chart from first-party GitHub data.
//
// GitHub restricted the public stargazers API to a repository's own admins and
// collaborators (announced 2026-06-30), which broke every third-party chart
// service — api.star-history.com now serves an error card instead of a chart.
// We own this repo, so we read the star data ourselves and commit the result,
// keeping the README free of any third-party image host.
//
// The committed dataset, not the drawing, is the source of truth: the SVG is a
// pure function of assets/star-history.json. That keeps the chart re-renderable
// with no credentials if the endpoint tightens further, keeps the weekly diff
// readable (dates, not shifted path coordinates), and lets `npm run validate`
// prove the two files agree.
//
// Run: node scripts/gen-star-history.mjs → refetch, rewrite both files
// node scripts/gen-star-history.mjs --render-only → redraw the SVG offline
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
// README-only assets, so they live in assets/ alone — unlike the banners, the
// GitHub Pages site under docs/ does not render them.
const OUT = join(ROOT, "assets", "star-history.svg");
const DATA = join(ROOT, "assets", "star-history.json");
const REPO = process.env.GITHUB_REPOSITORY ?? "hyhmrright/brooks-lint";
const W = 800, H = 400;
const PAD = { top: 44, right: 24, bottom: 48, left: 64 };
const PLOT_W = W - PAD.left - PAD.right;
const PLOT_H = H - PAD.top - PAD.bottom;
const ACCENT = "#3b82f6"; // matches the logo palette used by gen-banner.mjs
const FONT = `-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif`;
const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
// The stargazers endpoint is no longer public: it needs credentials that can
// read this repo. CI supplies GITHUB_TOKEN; locally we borrow the gh CLI's.
function resolveToken() {
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
try {
return execFileSync("gh", ["auth", "token"], { encoding: "utf8" }).trim();
} catch {
throw new Error(
"No GITHUB_TOKEN set and `gh auth token` failed. Since GitHub restricted " +
"the stargazers API, this script needs credentials for a repo admin or collaborator.",
);
}
}
// Returns the raw starred_at strings, oldest first — exactly what we commit.
async function fetchStarTimestamps(token) {
const stamps = [];
for (let page = 1; ; page++) {
const url = `https://api.github.com/repos/${REPO}/stargazers?per_page=100&page=${page}`;
const res = await fetch(url, {
headers: {
// The star+json media type is what adds starred_at to each entry.
Accept: "application/vnd.github.star+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "brooks-lint-gen-star-history",
},
});
// Past 400 pages GitHub answers 422 rather than paginating, so a repo above
// 40,000 stars can no longer be read in full — the committed dataset is what
// preserves the history when that day comes.
if (!res.ok) {
throw new Error(`GitHub API ${res.status} on page ${page}: ${(await res.text()).slice(0, 200)}`);
}
const batch = await res.json();
for (const entry of batch) {
// A missing starred_at means the star+json media type stopped being
// honoured. Fail loudly rather than plot NaN coordinates.
if (Number.isNaN(Date.parse(entry.starred_at))) {
throw new Error(`Stargazer without a usable starred_at on page ${page}.`);
}
stamps.push(entry.starred_at);
}
if (batch.length < 100) return stamps.sort((a, b) => Date.parse(a) - Date.parse(b));
}
}
export function readStamps() {
return JSON.parse(readFileSync(DATA, "utf8")).starredAt;
}
function writeStamps(stamps) {
// Deliberately no generated-at field: the file has to stay byte-identical when
// no star was added, or the workflow's "commit only when it moved" guard would
// fire every single run. Git already records when it last changed.
writeFileSync(DATA, `${JSON.stringify({ repo: REPO, starredAt: stamps }, null, 2)}\n`);
}
// Round the axis maximum up to a 1/2/5 × 10ⁿ step so tick labels stay readable.
function niceStep(max, targetTicks) {
const raw = max / targetTicks;
const mag = 10 ** Math.floor(Math.log10(raw));
for (const m of [1, 2, 5]) if (raw <= m * mag) return m * mag;
return 10 * mag;
}
// One tick per month, thinned out so labels never collide on a long history.
function monthTicks(from, to) {
const all = [];
const cursor = new Date(from);
cursor.setUTCDate(1);
cursor.setUTCHours(0, 0, 0, 0);
if (cursor.getTime() < from) cursor.setUTCMonth(cursor.getUTCMonth() + 1);
while (cursor.getTime() <= to) {
all.push(cursor.getTime());
cursor.setUTCMonth(cursor.getUTCMonth() + 1);
}
const stride = Math.ceil(all.length / 8) || 1;
return all.filter((_, i) => i % stride === 0);
}
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function monthLabel(ms, showYear) {
const d = new Date(ms);
const month = MONTHS[d.getUTCMonth()];
return showYear ? `${month} ${d.getUTCFullYear()}` : month;
}
// 1,392 points would bloat the committed SVG for no visible gain. Keep every
// nth sample plus the exact final point, so the headline count stays truthful.
function downsample(points, limit) {
if (points.length <= limit) return points;
const stride = Math.ceil(points.length / limit);
const kept = points.filter((_, i) => i % stride === 0);
if (kept.at(-1) !== points.at(-1)) kept.push(points.at(-1));
return kept;
}
export function render(stamps) {
const times = stamps.map((iso) => Date.parse(iso));
const total = times.length;
const t0 = times[0];
// The axis ends at the newest star rather than at "now". Anchoring it to the
// clock would shift every x coordinate on every run, so the workflow could
// never tell a real change from a redraw and would commit noise weekly.
const t1 = times.at(-1);
// Stars are whole numbers, so never let a tiny repo produce fractional ticks.
const step = Math.max(1, niceStep(total, 5));
const yMax = Math.ceil(total / step) * step;
const x = (ms) => PAD.left + ((ms - t0) / (t1 - t0)) * PLOT_W;
const y = (n) => PAD.top + PLOT_H - (n / yMax) * PLOT_H;
const points = downsample(
times.map((ms, i) => [ms, i + 1]),
300,
);
const line = points.map(([ms, n], i) => `${i === 0 ? "M" : "L"}${x(ms).toFixed(1)} ${y(n).toFixed(1)}`).join("");
const area = `${line}L${x(t1).toFixed(1)} ${y(0).toFixed(1)}L${x(t0).toFixed(1)} ${y(0).toFixed(1)}Z`;
const yTicks = [];
for (let n = 0; n <= yMax; n += step) yTicks.push(n);
const xTicks = monthTicks(t0, t1);
const spansYears = new Date(t0).getUTCFullYear() !== new Date(t1).getUTCFullYear();
const gridLines = yTicks
.map(
(n) =>
`<line x1="${PAD.left}" y1="${y(n).toFixed(1)}" x2="${PAD.left + PLOT_W}" y2="${y(n).toFixed(1)}" class="grid"/>` +
`<text x="${PAD.left - 10}" y="${(y(n) + 4).toFixed(1)}" class="tick" text-anchor="end">${n.toLocaleString("en-US")}</text>`,
)
.join("\n ");
const xLabels = xTicks
.map(
(ms, i) =>
`<text x="${x(ms).toFixed(1)}" y="${PAD.top + PLOT_H + 22}" class="tick" text-anchor="middle">` +
`${monthLabel(ms, spansYears || i === 0)}</text>`,
)
.join("\n ");
// Anchoring the axis to the newest star puts the final point exactly on the
// right edge, so the callout always hangs back inside the plot.
const lastX = PAD.left + PLOT_W;
const lastY = y(total);
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="Star history for ${esc(REPO)}: ${total} stars">
<style>
.bg { fill: #ffffff; }
.title { fill: #111827; font: 600 16px ${FONT}; }
.sub { fill: #6b7280; font: 400 12px ${FONT}; }
.tick { fill: #6b7280; font: 400 11px ${FONT}; }
.grid { stroke: #e5e7eb; stroke-width: 1; }
.axis { stroke: #d1d5db; stroke-width: 1; }
.total { fill: ${ACCENT}; font: 600 13px ${FONT}; }
@media (prefers-color-scheme: dark) {
.bg { fill: #0d1117; }
.title { fill: #e6edf3; }
.sub, .tick { fill: #8b949e; }
.grid { stroke: #21262d; }
.axis { stroke: #30363d; }
}
</style>
<defs>
<linearGradient id="fade" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${ACCENT}" stop-opacity="0.28"/>
<stop offset="100%" stop-color="${ACCENT}" stop-opacity="0.02"/>
</linearGradient>
</defs>
<rect width="${W}" height="${H}" class="bg"/>
<text x="${PAD.left}" y="26" class="title">Star History</text>
<text x="${W - PAD.right}" y="26" class="sub" text-anchor="end">${esc(REPO)}</text>
<g>
${gridLines}
</g>
<line x1="${PAD.left}" y1="${PAD.top + PLOT_H}" x2="${PAD.left + PLOT_W}" y2="${PAD.top + PLOT_H}" class="axis"/>
<path d="${area}" fill="url(#fade)"/>
<path d="${line}" fill="none" stroke="${ACCENT}" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="${lastX.toFixed(1)}" cy="${lastY.toFixed(1)}" r="4" fill="${ACCENT}"/>
<text x="${(lastX - 12).toFixed(1)}" y="${(lastY + 4).toFixed(1)}" class="total" text-anchor="end">${total.toLocaleString("en-US")}</text>
<g>
${xLabels}
</g>
</svg>
`;
}
async function main() {
const renderOnly = process.argv.includes("--render-only");
const stamps = renderOnly ? readStamps() : await fetchStarTimestamps(resolveToken());
// Two points are the minimum a time axis can span; one would divide by zero
// and silently write a chart full of NaN coordinates.
if (stamps.length < 2) throw new Error(`Only ${stamps.length} stargazer(s) for ${REPO} — refusing to write a chart.`);
if (!renderOnly) writeStamps(stamps);
writeFileSync(OUT, render(stamps));
console.log(`Wrote ${OUT}${stamps.length} stars through ${stamps.at(-1).slice(0, 10)}`);
}
// Importable so `npm run validate` can re-render from the committed data and
// prove the SVG is in sync; only a direct run touches the network or disk.
if (process.argv[1] === fileURLToPath(import.meta.url)) await main();
+7 -3
View File
@@ -13,8 +13,9 @@
# ./scripts/install.sh <platform> [--project]
# curl -fsSL https://raw.githubusercontent.com/hyhmrright/brooks-lint/main/scripts/install.sh | bash -s -- <platform>
#
# Platforms: opencode cursor windsurf antigravity pi kiro copilot droid gemini codex claude agents
# agents = the vendor-neutral ~/.agents/skills folder (read by Cursor, Copilot, pi, Gemini, Codex)
# Platforms: opencode cursor windsurf antigravity pi kiro copilot droid dsh gemini codex claude agents
# agents = the vendor-neutral ~/.agents/skills folder (read by Cursor, Copilot, pi, Gemini,
# Codex, and DeepSeek Harness)
#
# Flags:
# --project install into the current repo (./.<platform>/skills) instead of the global folder
@@ -24,7 +25,7 @@
set -euo pipefail
REPO_URL="https://github.com/hyhmrright/brooks-lint.git"
PLATFORMS="opencode cursor windsurf antigravity pi kiro copilot droid gemini codex claude agents"
PLATFORMS="opencode cursor windsurf antigravity pi kiro copilot droid dsh gemini codex claude agents"
err() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; }
info() { printf '\033[36m\033[0m %s\n' "$*"; }
@@ -59,6 +60,8 @@ global_dir() {
kiro) printf '%s' "$HOME/.kiro/skills" ;;
copilot) printf '%s' "$HOME/.copilot/skills" ;;
droid) printf '%s' "$HOME/.factory/skills" ;;
# DeepSeek Harness resolves its config root from $DSH_HOME, falling back to ~/.dsh.
dsh) printf '%s' "${DSH_HOME:-$HOME/.dsh}/skills" ;;
gemini) printf '%s' "$HOME/.gemini/skills" ;;
codex) printf '%s' "$HOME/.codex/skills" ;;
claude) printf '%s' "$HOME/.claude/skills" ;;
@@ -77,6 +80,7 @@ project_dir() {
kiro) printf '%s' "$PWD/.kiro/skills" ;;
copilot) printf '%s' "$PWD/.github/skills" ;;
droid) printf '%s' "$PWD/.factory/skills" ;;
dsh) printf '%s' "$PWD/.dsh/skills" ;;
gemini) printf '%s' "$PWD/.gemini/skills" ;;
codex) printf '%s' "$PWD/.codex/skills" ;;
claude) printf '%s' "$PWD/.claude/skills" ;;
+64
View File
@@ -0,0 +1,64 @@
/**
* Platform-inventory helpers shared by validate-repo.mjs and its tests.
*
* Nothing here hardcodes a platform or a translation. The platforms carrying an
* install-table row are discovered from the docs/<name>-setup.md files on disk,
* the documents that must show that table are discovered from README*.md, and
* the installer's own list is parsed out of scripts/install.sh — so a new
* platform, or a seventh language, is covered on arrival. Keeping a separate
* hand-maintained list is what let the localized README badges go stale before
* (see version-refs.mjs).
*/
import { readdirSync } from "node:fs";
import path from "node:path";
/**
* Every document that carries the per-platform install table: all README
* translations, plus the getting-started guide, which is the one non-README
* page with a platform table of its own.
*/
export function platformDocs(root) {
const readmes = readdirSync(root)
.filter((file) => file.startsWith("README") && file.endsWith(".md"))
.sort();
return [...readmes, path.join("docs", "getting-started.md")];
}
/** Every per-platform setup guide, as bare filenames. */
export function setupGuides(root) {
return readdirSync(path.join(root, "docs"))
.filter((file) => file.endsWith("-setup.md"))
.sort();
}
/**
* Setup-guide filenames linked from one document, normalized so a README's
* `docs/kiro-setup.md` and getting-started's sibling `kiro-setup.md` compare
* equal. Returns a sorted, de-duplicated array.
*/
export function linkedSetupGuides(text) {
const links = text.matchAll(/\((?:docs\/)?([a-z0-9-]+-setup\.md)\)/g);
return [...new Set([...links].map((match) => match[1]))].sort();
}
/**
* The installer's platform list plus the platforms each directory-mapping
* function actually handles. A platform in PLATFORMS with no case arm fails
* `install.sh <platform>` with "unknown platform"; a case arm missing from
* PLATFORMS is invisible in --list and the help text.
*/
export function parseInstallerPlatforms(text) {
const declared = text.match(/^PLATFORMS="([^"]*)"/m)?.[1].trim();
return {
declared: declared ? declared.split(/\s+/) : [],
global: caseArms(text, "global_dir"),
project: caseArms(text, "project_dir"),
};
}
/** Platform names matched by the `case` arms of one shell function. */
function caseArms(text, fnName) {
const body = text.match(new RegExp(`^${fnName}\\(\\) \\{$([\\s\\S]*?)^\\}$`, "m"))?.[1] ?? "";
return [...body.matchAll(/^\s+([a-z][a-z0-9-]*)\)/gm)].map((match) => match[1]);
}
+1 -1
View File
@@ -81,7 +81,7 @@ for (const scenario of scenarios) {
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
});
aiText = message.content[0]?.text ?? "";
aiText = message.content.find((block) => block.type === "text")?.text ?? "";
verdict = classify(scenario, aiText);
} catch (err) {
error = err.message;
+48
View File
@@ -15,6 +15,8 @@ import {
} from "./frontmatter.mjs";
import { GUIDE_BY_MODE, VALID_MODES } from "./assemble-prompt.mjs";
import { versionRefs } from "./version-refs.mjs";
import { platformDocs, setupGuides, linkedSetupGuides, parseInstallerPlatforms } from "./platforms.mjs";
import { render as renderStarHistory, readStamps as readStarStamps } from "./gen-star-history.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");
@@ -325,6 +327,49 @@ function checkAgentsDocs() {
}
}
// Every docs/<name>-setup.md must be linked from all six READMEs and the
// getting-started table. A platform added to one language and forgotten in the
// others was previously caught only by hand.
function checkPlatformDocs() {
const guides = setupGuides(root);
check(guides.length > 0, "docs/ should contain at least one <platform>-setup.md guide");
for (const file of platformDocs(root)) {
const linked = linkedSetupGuides(readText(file));
for (const guide of guides) {
check(linked.includes(guide), `${file} is missing an install-table link to docs/${guide}`);
}
for (const link of linked) {
check(guides.includes(link), `${file} links to docs/${link}, which does not exist`);
}
}
}
function checkInstallerPlatforms() {
const { declared, global: globalArms, project } = parseInstallerPlatforms(readText("scripts/install.sh"));
check(declared.length > 0, "scripts/install.sh should declare a PLATFORMS list");
for (const [arms, fn] of [[globalArms, "global_dir"], [project, "project_dir"]]) {
for (const platform of declared) {
check(arms.includes(platform), `scripts/install.sh ${fn}() has no path for PLATFORMS entry '${platform}'`);
}
for (const platform of arms) {
check(declared.includes(platform), `scripts/install.sh ${fn}() maps '${platform}', which PLATFORMS omits`);
}
}
}
// assets/star-history.svg is a pure function of assets/star-history.json, so a
// mismatch means the chart was hand-edited or the data moved without a redraw.
// Re-rendering here needs no credentials, which is the point of committing the
// data rather than the drawing alone.
function checkStarHistory() {
check(
renderStarHistory(readStarStamps()) === readText("assets/star-history.svg"),
"assets/star-history.svg does not match assets/star-history.json — rerun `node scripts/gen-star-history.mjs --render-only`",
);
}
function checkSecurity() {
const security = readText("SECURITY.md");
check(!security.includes("<!--"), "SECURITY.md still contains placeholder content");
@@ -373,7 +418,10 @@ checkStepAlignment();
checkEvalSuite();
checkContributing();
checkAgentsDocs();
checkPlatformDocs();
checkInstallerPlatforms();
checkSecurity();
checkStarHistory();
checkHookOutput();
// ── Report ─────────────────────────────────────────────────────────────────
@@ -31,6 +31,7 @@ import { reportToSarif } from "./sarif.mjs";
import { severityBreached, isRegression } from "./ci-gate.mjs";
import { summarize } from "./benchmark.mjs";
import { versionRefs } from "./version-refs.mjs";
import { linkedSetupGuides, parseInstallerPlatforms } from "./platforms.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -905,6 +906,70 @@ test("patterns match the real badge and JSON-LD shapes", () => {
});
});
console.log("\nlinkedSetupGuides");
test("collects guides from README-style and sibling-style links alike", () => {
const text = "| Kiro | [setup](docs/kiro-setup.md) |\n| pi | [pi-setup.md](pi-setup.md) |";
assert.deepEqual(linkedSetupGuides(text), ["kiro-setup.md", "pi-setup.md"]);
});
test("de-duplicates a guide linked more than once", () => {
const text = "[a](docs/dsh-setup.md) … [b](dsh-setup.md)";
assert.deepEqual(linkedSetupGuides(text), ["dsh-setup.md"]);
});
test("returns an empty array when no guide is linked", () => {
assert.deepEqual(linkedSetupGuides("no links here"), []);
});
console.log("\nparseInstallerPlatforms");
const INSTALLER_FIXTURE = [
'PLATFORMS="kiro dsh"',
"",
"global_dir() {",
" case $1 in",
" kiro) printf '%s' \"$HOME/.kiro/skills\" ;;",
" # DeepSeek Harness resolves its config root from $DSH_HOME.",
" dsh) printf '%s' \"${DSH_HOME:-$HOME/.dsh}/skills\" ;;",
" *) return 1 ;;",
" esac",
"}",
"",
"project_dir() {",
" case $1 in",
" kiro) printf '%s' \"$PWD/.kiro/skills\" ;;",
" *) return 1 ;;",
" esac",
"}",
].join("\n");
test("reads the declared list and both directory mappings", () => {
const parsed = parseInstallerPlatforms(INSTALLER_FIXTURE);
assert.deepEqual(parsed.declared, ["kiro", "dsh"]);
assert.deepEqual(parsed.global, ["kiro", "dsh"]);
});
test("omits a platform whose case arm is missing, so the validator can catch it", () => {
// project_dir() in the fixture handles kiro but not dsh — running
// `install.sh dsh --project` would die with "unknown platform".
assert.deepEqual(parseInstallerPlatforms(INSTALLER_FIXTURE).project, ["kiro"]);
});
test("skips comments and the catch-all arm", () => {
const { global: arms } = parseInstallerPlatforms(INSTALLER_FIXTURE);
assert.ok(!arms.includes("*"));
assert.equal(arms.length, 2);
});
test("parses the real installer, proving the patterns still match", () => {
const installer = readFileSync(path.join(__dirname, "install.sh"), "utf8");
const { declared, global: globalArms, project } = parseInstallerPlatforms(installer);
assert.ok(declared.length >= 12, `expected the full platform list, got ${declared.length}`);
assert.deepEqual(new Set(globalArms), new Set(declared));
assert.deepEqual(new Set(project), new Set(declared));
});
// ── Integration: validate-repo.mjs passes against current repo ─────────────
console.log("\nvalidate-repo integration");