verify-subsystem-pages.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. /**
  2. * Doc-sync gate for package-group subsystem references. Every package group
  3. * either links at least one existing `docs/subsystems/` page from its English
  4. * group README or carries an explicit, justified exemption below.
  5. */
  6. import { existsSync, globSync, readFileSync } from 'node:fs'
  7. import { resolve, sep } from 'node:path'
  8. import { parseMarkdown, visitMarkdown } from './markdown.ts'
  9. const root = resolve(import.meta.dirname, '..')
  10. /**
  11. * Package groups that do not own a standalone subsystem reference. Reasons
  12. * are reviewable policy: a new group cannot silently inherit an exemption.
  13. */
  14. export const GROUPS_WITHOUT_SUBSYSTEM_PAGE: Readonly<Record<string, string>> = {
  15. acp: 'Protocol transport entry point; the server package README owns its interoperability contract.',
  16. api: 'Remote transport and BFF assembly; Typert and the package READMEs own the underlying contracts.',
  17. boot: 'Shared application-bin boot library rather than a runtime subsystem.',
  18. bundle: 'Composition patch carriers whose mounted packages own all runtime contracts.',
  19. e2b: 'Provider implementations of the filesystem and subprocess subsystems, not a new capability contract.',
  20. examples: 'Non-product demonstration compositions whose mounted packages own all runtime contracts.',
  21. experimental: 'Empty staging group; promoted packages move to their product-role group before release.',
  22. feedback: 'One command producer and inline log-event payload; its package README and persistence catalog own the complete contract.',
  23. hooks: 'External hook-protocol bridges over existing interception points, not a new Harness service.',
  24. identity: 'Shared anonymous correlation values rather than authenticated account or authorization behavior.',
  25. mcp: 'Integration adapter that contributes external tools through the existing tool registry.',
  26. sdk: 'Out-of-process protocol and client packages whose package READMEs own the SDK contracts.',
  27. util: 'Low-level primitives whose business semantics remain with their consuming subsystems.',
  28. }
  29. /** Result of auditing package-group subsystem documentation. */
  30. export interface SubsystemPageAudit {
  31. /** Package groups discovered from group READMEs or child package manifests. */
  32. readonly groups: number
  33. /** Groups carrying at least one direct subsystem-page link. */
  34. readonly linked: number
  35. /** Groups covered by an explicit no-page policy. */
  36. readonly exempt: number
  37. /** Actionable contract violations. */
  38. readonly violations: readonly string[]
  39. }
  40. /** Normalize one filesystem glob result to repository slash form. */
  41. function normalize(path: string): string {
  42. return path.split(sep).join('/')
  43. }
  44. /** Extract the package-group segment from a repository-relative path. */
  45. function groupOf(path: string): string {
  46. const group = path.split('/')[1]
  47. if (group === undefined || group.length === 0) throw new Error(`invalid package path: ${path}`)
  48. return group
  49. }
  50. /** Return canonical subsystem-page targets linked by one group README. */
  51. function subsystemLinks(source: string): string[] {
  52. const links = new Set<string>()
  53. visitMarkdown(parseMarkdown(source), (node) => {
  54. if (node.type !== 'link') return
  55. const match = /^\.\.\/\.\.\/docs\/subsystems\/([^/#?]+\.md)(?:#[^?#]*)?$/.exec(node.url)
  56. const page = match?.[1]
  57. if (page !== undefined && page !== 'README.md' && !page.endsWith('.zh.md')) links.add(`docs/subsystems/${page}`)
  58. })
  59. return [...links].sort()
  60. }
  61. /**
  62. * Audit package-group subsystem ownership for one repository tree.
  63. * @param scanRoot - repository root containing `packages/` and `docs/`.
  64. * @param exemptions - groups intentionally carrying no subsystem-page link.
  65. * @returns counts plus every actionable violation.
  66. */
  67. export function auditSubsystemPages(
  68. scanRoot: string = root,
  69. exemptions: Readonly<Record<string, string>> = GROUPS_WITHOUT_SUBSYSTEM_PAGE,
  70. ): SubsystemPageAudit {
  71. const readmes = globSync('packages/*/README.md', { cwd: scanRoot }).map(normalize).sort()
  72. const manifests = globSync('packages/*/*/package.json', { cwd: scanRoot }).map(normalize).sort()
  73. const groups = new Set([...readmes, ...manifests].map(groupOf))
  74. const violations: string[] = []
  75. let linked = 0
  76. let exempt = 0
  77. for (const [group, reason] of Object.entries(exemptions)) {
  78. if (!groups.has(group)) {
  79. violations.push(`exemption ${group}: no matching package group; remove the stale entry`)
  80. }
  81. if (reason.trim().length === 0) {
  82. violations.push(`exemption ${group}: missing justification for omitting a subsystem page`)
  83. }
  84. }
  85. for (const group of [...groups].sort()) {
  86. const readme = `packages/${group}/README.md`
  87. const readmePath = resolve(scanRoot, readme)
  88. if (!existsSync(readmePath)) {
  89. violations.push(`${readme}: package group has no group README declaring subsystem ownership`)
  90. continue
  91. }
  92. const links = subsystemLinks(readFileSync(readmePath, 'utf8'))
  93. const isExempt = Object.hasOwn(exemptions, group)
  94. if (links.length === 0) {
  95. if (isExempt) {
  96. exempt += 1
  97. } else {
  98. violations.push(
  99. `${readme}: no reader-visible direct docs/subsystems/*.md link; add the owning page and link,`
  100. + ' or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry',
  101. )
  102. }
  103. continue
  104. }
  105. linked += 1
  106. if (isExempt) {
  107. violations.push(`${readme}: links a subsystem page but remains exempt; remove the stale exemption`)
  108. }
  109. for (const page of links) {
  110. if (!existsSync(resolve(scanRoot, page))) {
  111. violations.push(`${readme}: linked subsystem page does not exist: ${page}`)
  112. }
  113. }
  114. }
  115. return { groups: groups.size, linked, exempt, violations }
  116. }
  117. /** Run the repository audit as a standalone doc-sync gate. */
  118. function main(): void {
  119. const audit = auditSubsystemPages()
  120. if (audit.violations.length > 0) {
  121. console.error('verify-subsystem-pages: package-group documentation violations found:')
  122. for (const violation of audit.violations) console.error(` ${violation}`)
  123. process.exit(1)
  124. }
  125. console.log(
  126. `verify-subsystem-pages: ${String(audit.groups)} group(s) checked`
  127. + ` (${String(audit.linked)} linked, ${String(audit.exempt)} explicitly exempt), all conform.`,
  128. )
  129. }
  130. if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) main()