verify-package-readme-summaries.ts 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. /** Enforce the English package README Summary entry-length limit. */
  2. import { globSync, readFileSync } from 'node:fs'
  3. import { resolve } from 'node:path'
  4. const root = resolve(import.meta.dirname, '..')
  5. /** Maximum `wc -w`-style length of an English package README Summary. */
  6. export const MAX_PACKAGE_README_SUMMARY_WORDS = 100
  7. const PACKAGE_README_PATTERNS = [
  8. 'packages/README.md',
  9. 'packages/*/README.md',
  10. 'packages/*/*/README.md',
  11. ] as const
  12. /** `wc -w` equivalent used by the documentation budget gate. */
  13. function countWords(text: string): number {
  14. return text.split(/\s+/u).filter(Boolean).length
  15. }
  16. /** Extract one H2 section body without consuming the next H2. */
  17. function h2Body(source: string, heading: string): string | undefined {
  18. const escaped = heading.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
  19. const match = new RegExp(`^## ${escaped}\\s*\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'mu').exec(source)
  20. return match?.[1]?.trim()
  21. }
  22. /** Read the package README kind for a diagnostic template link. */
  23. function readKind(source: string): string | undefined {
  24. return /^kind:\s*["']?([a-z-]+)["']?\s*$/mu.exec(source)?.[1]
  25. }
  26. /**
  27. * Report Summary length violations for one English package README.
  28. * @param file - Repository-relative README path.
  29. * @param source - Complete README source.
  30. * @returns Diagnostics for a missing or oversized Summary.
  31. */
  32. export function packageReadmeSummaryErrors(file: string, source: string): string[] {
  33. const summary = h2Body(source, 'Summary')
  34. if (summary === undefined) return [`${file}: missing \`## Summary\``]
  35. const words = countWords(summary)
  36. if (words <= MAX_PACKAGE_README_SUMMARY_WORDS) return []
  37. const kind = readKind(source)
  38. const template = kind === undefined
  39. ? '.agents/skills/dsh-doc/templates/'
  40. : `.agents/skills/dsh-doc/templates/${kind}.md`
  41. return [
  42. `${file}: Summary has ${String(words)} words; the limit is ${String(MAX_PACKAGE_README_SUMMARY_WORDS)}. Read .agents/skills/dsh-doc/SKILL.md and ${template} before rewriting it.`,
  43. ]
  44. }
  45. /** Find every authored English package README covered by the kind templates. */
  46. function packageReadmes(): string[] {
  47. return PACKAGE_README_PATTERNS
  48. .flatMap(pattern => globSync(pattern, { cwd: root, exclude: ['**/node_modules/**'] }))
  49. .map(file => file.replaceAll('\\', '/'))
  50. .sort()
  51. }
  52. if (import.meta.main) {
  53. const files = packageReadmes()
  54. const failures = files.length === 0
  55. ? ['no English package READMEs found; the scan is empty or narrowed']
  56. : files.flatMap(file => packageReadmeSummaryErrors(file, readFileSync(resolve(root, file), 'utf8')))
  57. if (failures.length > 0) {
  58. console.error('verify-package-readme-summaries: violations found:')
  59. for (const failure of failures) console.error(` ${failure}`)
  60. process.exitCode = 1
  61. } else {
  62. console.log(`verify-package-readme-summaries: ${String(files.length)} English package README Summaries are within ${String(MAX_PACKAGE_README_SUMMARY_WORDS)} words.`)
  63. }
  64. }