-
Notifications
You must be signed in to change notification settings - Fork 1
feat(hygiene): bundled pre-push self-check helper for tick shards #3716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| #!/usr/bin/env bun | ||
| // check-shard-before-push.ts — bundled pre-push self-checks for tick shards | ||
| // | ||
| // Run on a single shard (or multiple) before pushing. Wraps three CI gates | ||
| // that the shard would otherwise hit only after push: | ||
| // | ||
| // 1. MD032 paragraph-immediately-followed-by-bullet detection | ||
| // (markdownlint catches this; running before push is faster feedback) | ||
| // 2. `markdownlint-cli2` for the broad markdown lint surface | ||
| // (MD038 no-space-in-code, MD024, etc.) | ||
| // 3. `audit-tick-shard-relative-paths` for broken relative-path links | ||
| // (the dedicated audit gate — same as CI runs in `--enforce --baseline` | ||
| // mode, but here in detect-only-for-this-file mode) | ||
| // | ||
| // This is a DX helper, not a CI gate. CI gates remain authoritative; this | ||
| // script just shortens the local-feedback loop from "push + wait for CI" | ||
| // to "single command + immediate output." | ||
| // | ||
| // Usage: | ||
| // | ||
| // bun tools/hygiene/check-shard-before-push.ts <shard-path>... | ||
| // bun tools/hygiene/check-shard-before-push.ts docs/hygiene-history/ticks/2026/05/16/0340Z.md | ||
| // | ||
| // Exit codes: | ||
| // | ||
| // 0 all checks passed on all input files | ||
| // 1 one or more checks failed | ||
| // 64 argument error (no files / non-file inputs) | ||
| // | ||
| // Composes with: audit-tick-shard-relative-paths.ts (relative-path audit), | ||
| // audit-md032-plus-linestart.ts (sibling `+`-marker MD032 check), | ||
| // AUDIT-LIFECYCLE.md (the lifecycle template that motivates pre-push catches). | ||
|
|
||
| import { existsSync, statSync, readFileSync } from "node:fs"; | ||
| import { resolve } from "node:path"; | ||
| import { spawnSync } from "node:child_process"; | ||
|
|
||
| // arg parsing | ||
|
|
||
| interface Args { | ||
| files: readonly string[]; | ||
| } | ||
|
|
||
| function parseArgs(argv: readonly string[]): Args { | ||
| if (argv.length === 0) { | ||
| process.stderr.write("usage: check-shard-before-push.ts <shard-path>...\n"); | ||
| process.exit(64); | ||
| } | ||
| return { files: argv }; | ||
| } | ||
|
|
||
| // MD032 paragraph-immediately-followed-by-bullet detection. | ||
| // The same `awk`-shaped scan I've been running per-tick this session, | ||
| // ported to TS for the bundled-helper interface. | ||
|
|
||
| interface Md032Finding { | ||
| readonly file: string; | ||
| readonly line: number; | ||
| readonly paragraph: string; | ||
| readonly bullet: string; | ||
| } | ||
|
|
||
| function checkMd032(file: string): Md032Finding[] { | ||
| const text = readFileSync(file, "utf8"); | ||
| const lines = text.split("\n"); | ||
| const findings: Md032Finding[] = []; | ||
| for (let i = 1; i < lines.length; i++) { | ||
| const prev = lines[i - 1]!; | ||
| const cur = lines[i]!; | ||
| // Trigger: cur starts with "- " bullet AND prev is non-blank | ||
| // AND prev is not itself a structural marker (#, >, *, -, |, ```) | ||
| if (!cur.startsWith("- ")) continue; | ||
| if (prev.trim() === "") continue; | ||
| if (/^[#>*\-|`]/.test(prev)) continue; | ||
|
AceHack marked this conversation as resolved.
Comment on lines
+93
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The custom Useful? React with 👍 / 👎. |
||
| findings.push({ file, line: i + 1, paragraph: prev, bullet: cur }); | ||
|
AceHack marked this conversation as resolved.
|
||
| } | ||
| return findings; | ||
| } | ||
|
|
||
| // markdownlint-cli2 wrapper. | ||
| // Returns true on clean exit, false on findings. Stdout/stderr are echoed | ||
| // directly so the user sees the same output they'd see from the CI gate. | ||
|
|
||
| function runMarkdownlint(file: string): boolean { | ||
| // eslint-disable-next-line sonarjs/no-os-command-from-path -- bun invoked as explicit args array; no shell; no user input beyond the file path which is validated upfront. | ||
| const r = spawnSync("bun", ["x", "markdownlint-cli2", file], { | ||
|
AceHack marked this conversation as resolved.
|
||
| stdio: ["ignore", "inherit", "inherit"], | ||
| }); | ||
| return r.status === 0; | ||
| } | ||
|
|
||
| // relative-path-audit wrapper. | ||
| // The audit's `--files` mode is detect-only by default (no --enforce), so | ||
| // we get the findings list. Exit 0 with findings == clean; non-zero = | ||
| // argument error. | ||
|
|
||
| function runRelativePathAudit(file: string): boolean { | ||
| // eslint-disable-next-line sonarjs/no-os-command-from-path -- bun invoked as explicit args array; no shell; no user input beyond the file path which is validated upfront. | ||
| const r = spawnSync( | ||
| "bun", | ||
| [ | ||
| "tools/hygiene/audit-tick-shard-relative-paths.ts", | ||
| "--files", | ||
| file, | ||
| ], | ||
| { encoding: "utf8" }, | ||
| ); | ||
| const out = r.stdout ?? ""; | ||
| const err = r.stderr ?? ""; | ||
| // Output format: "ok: scanned N tick shards; 0 broken relative-path links\n" | ||
| // OR "scanned N tick shards; M broken relative-path links:\n\n file:line ...\n" | ||
| if (out.includes("0 broken relative-path links")) { | ||
| return true; | ||
| } | ||
|
AceHack marked this conversation as resolved.
Outdated
|
||
| // findings present — print them | ||
| process.stdout.write(out); | ||
| if (err) process.stderr.write(err); | ||
| return false; | ||
| } | ||
|
|
||
| // main | ||
|
|
||
| export function main(argv: readonly string[]): 0 | 1 | 64 { | ||
| const args = parseArgs(argv); | ||
|
AceHack marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Validate inputs (per the validation pattern from the audit script). | ||
| const bad: { path: string; reason: string }[] = []; | ||
| for (const f of args.files) { | ||
| const abs = resolve(f); | ||
| if (!existsSync(abs)) { | ||
| bad.push({ path: f, reason: "not found" }); | ||
| continue; | ||
| } | ||
| try { | ||
| if (!statSync(abs).isFile()) { | ||
| bad.push({ path: f, reason: "not a regular file" }); | ||
| } | ||
| } catch (e) { | ||
| bad.push({ path: f, reason: `stat failed (${(e as Error).message})` }); | ||
| } | ||
| } | ||
| if (bad.length > 0) { | ||
| for (const b of bad) process.stderr.write(`input ${b.reason}: ${b.path}\n`); | ||
| return 64; | ||
| } | ||
|
|
||
| let anyFailed = false; | ||
| for (const f of args.files) { | ||
| process.stdout.write(`=== ${f}\n`); | ||
|
|
||
| // 1. MD032 | ||
| const md032 = checkMd032(f); | ||
| if (md032.length === 0) { | ||
| process.stdout.write(" ok: MD032 (paragraph-before-bullet)\n"); | ||
| } else { | ||
| anyFailed = true; | ||
| process.stdout.write(` FAIL: MD032 (${md032.length} finding${md032.length === 1 ? "" : "s"}):\n`); | ||
| for (const m of md032) { | ||
| process.stdout.write(` line ${m.line}: '${m.paragraph.slice(0, 60)}' → '${m.bullet.slice(0, 60)}'\n`); | ||
| } | ||
| } | ||
|
|
||
| // 2. markdownlint | ||
| process.stdout.write(" running: markdownlint-cli2 ...\n"); | ||
| if (runMarkdownlint(f)) { | ||
| process.stdout.write(" ok: markdownlint\n"); | ||
| } else { | ||
| anyFailed = true; | ||
| process.stdout.write(" FAIL: markdownlint\n"); | ||
| } | ||
|
|
||
| // 3. relative-path audit | ||
| process.stdout.write(" running: audit-tick-shard-relative-paths ...\n"); | ||
| if (runRelativePathAudit(f)) { | ||
| process.stdout.write(" ok: relative-path audit\n"); | ||
| } else { | ||
| anyFailed = true; | ||
| process.stdout.write(" FAIL: relative-path audit\n"); | ||
| } | ||
| } | ||
|
|
||
| return anyFailed ? 1 : 0; | ||
| } | ||
|
|
||
| if (import.meta.main) { | ||
| process.exit(main(process.argv.slice(2))); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.