| 681 | |
| 682 | // BLOCKING. Returns a process exit code (0 ok, 1 on a broken link). |
| 683 | function runLinksCheck({ |
| 684 | repoRoot = process.cwd(), |
| 685 | minLinks = MIN_EXPECTED_LINKS, |
| 686 | minFiles = MIN_EXPECTED_LINK_FILES, |
| 687 | maxScanBytes = MAX_SCAN_BYTES, |
| 688 | } = {}) { |
| 689 | const files = walkTextFiles(repoRoot); |
| 690 | const links = []; |
| 691 | const skipped = []; |
| 692 | for (const file of files) { |
| 693 | const content = readFileOrNull(path.join(repoRoot, file), { maxBytes: maxScanBytes }); |
| 694 | if (content === null) { |
| 695 | // A documentation file that could not be read is a hole in the corpus, |
| 696 | // not a pass. Reporting it keeps this consistent with the fail-closed |
| 697 | // corpus floor below, which can only catch a scan that broke wholesale |
| 698 | // and would miss individual documents being skipped in silence. |
| 699 | if (DOC_EXTENSIONS.test(file)) skipped.push(file); |
| 700 | continue; |
| 701 | } |
| 702 | // Cheap prefilter: most files mention the slug nowhere. |
| 703 | if (!content.includes(REPO_SLUG)) continue; |
| 704 | for (const l of extractRepoLinks(content)) links.push({ ...l, file }); |
| 705 | } |
| 706 | |
| 707 | for (const file of skipped) { |
| 708 | emitWarning( |
| 709 | file, |
| 710 | `Skipped by the link scan (unreadable, binary, or larger than ` + |
| 711 | `${maxScanBytes} bytes), so its in-repo links were NOT verified.` |
| 712 | ); |
| 713 | } |
| 714 | |
| 715 | const fileCount = new Set(links.map((l) => l.file)).size; |
| 716 | const floor = checkCorpusFloor(links.length, fileCount, minLinks, minFiles); |
| 717 | if (floor) { |
| 718 | emitError(null, `In-repo link check failed: ${floor}`); |
| 719 | return 1; |
| 720 | } |
| 721 | |
| 722 | const { errors, warnings } = validateRepoLinks(links, (p) => probeKind(repoRoot, p)); |
| 723 | for (const err of errors) emitError(err.file, err.message, err.line); |
| 724 | for (const warn of warnings) emitWarning(warn.file, warn.message, warn.line); |
| 725 | |
| 726 | if (errors.length === 0) { |
| 727 | const unique = new Set(links.map((l) => l.repoPath)).size; |
| 728 | console.log( |
| 729 | `In-repo link check passed: ${links.length} link(s) across ` + |
| 730 | `${fileCount} file(s) resolve to ` + |
| 731 | `${unique} existing path(s)` + |
| 732 | (warnings.length || skipped.length |
| 733 | ? `, with ${warnings.length} blob/tree kind warning(s) and ` + |
| 734 | `${skipped.length} unscanned doc(s).` |
| 735 | : ".") |
| 736 | ); |
| 737 | return 0; |
| 738 | } |
| 739 | emitError( |
| 740 | null, |