@attalabs/vinaya 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +13 -3
  2. package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
  3. package/aeg-root/contracts/brief-developer.md +1 -0
  4. package/aeg-root/contracts/developer-reviewer.md +3 -3
  5. package/aeg-root/contracts/reviewer-archivist.md +3 -3
  6. package/aeg-root/enforcement.md +26 -20
  7. package/aeg-root/process.md +8 -4
  8. package/aeg-root/roles/archivist.md +2 -2
  9. package/aeg-root/roles/brief-author.md +9 -3
  10. package/aeg-root/roles/developer.md +63 -31
  11. package/aeg-root/roles/planner.md +1 -1
  12. package/aeg-root/roles/principal.md +14 -0
  13. package/aeg-root/roles/reviewer.md +32 -15
  14. package/aeg-root/roles/security.md +15 -5
  15. package/aeg-root/skills/aeg/SKILL.md +9 -5
  16. package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
  17. package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
  18. package/aeg-root/state-machine.md +9 -11
  19. package/aeg-root/templates/brief-template.md +6 -6
  20. package/aeg-root/templates/pr-report-template.md +10 -7
  21. package/aeg-root/tranche-model.md +6 -2
  22. package/dist/checks/bin/check-body-bare-digits.js +552 -139
  23. package/dist/checks/bin/check-branch-topology.js +603 -144
  24. package/dist/checks/bin/check-brief-shape.js +593 -142
  25. package/dist/checks/bin/check-changeset-coverage.js +935 -161
  26. package/dist/checks/bin/check-closes-n.js +603 -144
  27. package/dist/checks/bin/check-coherence.js +614 -147
  28. package/dist/checks/bin/check-dead-branch-push.js +552 -139
  29. package/dist/checks/bin/check-dispatch-readiness.js +612 -145
  30. package/dist/checks/bin/check-doc-coverage-push.js +689 -218
  31. package/dist/checks/bin/check-doc-coverage.js +689 -218
  32. package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
  33. package/dist/checks/bin/check-doctrine-portability.js +684 -146
  34. package/dist/checks/bin/check-evidence-fresh.js +994 -174
  35. package/dist/checks/bin/check-exec-bits.js +4931 -0
  36. package/dist/checks/bin/check-first-push-dispatch.js +603 -144
  37. package/dist/checks/bin/check-issue-assignment.js +603 -144
  38. package/dist/checks/bin/check-main-branch-refusal.js +552 -139
  39. package/dist/checks/bin/check-no-disk-state.js +552 -139
  40. package/dist/checks/bin/check-pr-report-density.js +4461 -0
  41. package/dist/checks/bin/check-quoted-command.js +621 -155
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
  43. package/dist/checks/bin/check-registry-gates.js +595 -144
  44. package/dist/checks/bin/check-retired-vocabulary.js +617 -154
  45. package/dist/checks/bin/check-review-gate.js +670 -141
  46. package/dist/checks/bin/check-single-plan-pr.js +552 -139
  47. package/dist/checks/bin/check-test-plan.js +553 -140
  48. package/dist/checks/bin/check-token-collection-wired.js +552 -139
  49. package/dist/checks/bin/check-token-report.js +552 -139
  50. package/dist/checks/bin/check-workspace-escape.js +985 -145
  51. package/dist/index.js +8500 -5518
  52. package/package.json +1 -1
  53. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
  54. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
  55. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
  56. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
  57. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
  58. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
  59. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
  60. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
  61. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
  62. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
  63. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
  64. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
  65. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
  66. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
  67. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
  68. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
  69. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
  70. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
  71. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
  72. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
  73. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
  74. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
  75. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
  76. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
  77. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
  78. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
  79. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
  80. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
  81. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
  82. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
  83. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
  84. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
  85. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
  86. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
  87. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
  88. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
  89. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
  90. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
  91. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
  92. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
  93. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
  94. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
  95. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
  96. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
  97. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
  98. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
  99. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
  100. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
  101. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
  102. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
  103. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
  104. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
  105. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
  106. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
  107. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
  108. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
  109. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
  110. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
  111. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
  112. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
  113. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
  114. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
  115. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
  116. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
  117. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
  118. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
  119. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
  120. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
  121. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
  122. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
  123. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
  124. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  125. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  126. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  127. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  128. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  129. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  131. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  132. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  133. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  135. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  143. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  144. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  145. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  147. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  148. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  149. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  150. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  151. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  152. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  153. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  154. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  155. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  156. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  157. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  158. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  159. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  160. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  161. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  162. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  163. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
  164. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  165. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  166. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  167. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  168. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  169. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1mcitst._.js} +1 -1
  170. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  171. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  172. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  173. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  174. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
  175. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  176. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  177. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  178. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  179. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  180. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  181. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
  182. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
  183. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
  184. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  185. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  186. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
  187. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
  188. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * C6 — the docs-coherence check (state-machine.md Section 15c). Pure: given
3
+ * every parsed doc entry from `aeg-root/` and the model-backed surfaced-path
4
+ * allowlist (`modelBackedDocPaths`, passed in because deriving
5
+ * it requires reading doctrine, which this pure module never does), asserts:
6
+ * (a) every surfaced doc is reachable in the doc-nav tree the docs engine
7
+ * (`buildDocNav`) would build for the surfaced set;
8
+ * (b) no surfaced doc's `parent:` reference points at a doc that doesn't
9
+ * exist, or exists but isn't itself surfaced;
10
+ * (c) every relative `.md` link between two surfaced docs resolves to
11
+ * another surfaced doc.
12
+ *
13
+ * (a) and (b) mirror the real parent/child resolution
14
+ * `apps/vinaya/web/src/lib/docs/nest-doc-children.ts` performs when building
15
+ * Vinaya's live `/docs` nav (a Portal route since `vinaya-pages-v1` task 8;
16
+ * that file is read-only reference here, never imported): a doc whose
17
+ * `parent:` frontmatter points at
18
+ * a nonexistent (or excluded) slug is silently dropped from that nav's flat
19
+ * list — reachable neither at the top level nor as anyone's child. That
20
+ * silent-drop is the exact defect this check exists to catch.
21
+ */
22
+
23
+ import { deriveTitle, type ParsedDoc } from './parse-doc'
24
+ import { isSurfacedDoc } from './surfaced-manifest'
25
+ import type { Doc, DocFrontmatter } from './types'
26
+
27
+ export type DocsCoherenceEntry = {
28
+ /** Path relative to `aeg-root/`, e.g. `roles/developer.md`. */
29
+ relPath: string
30
+ frontmatter: DocFrontmatter
31
+ body: string
32
+ firstH1?: string
33
+ }
34
+
35
+ export type DocsCoherenceResult = { errors: string[]; notes: string[] }
36
+
37
+ function slugOf(relPath: string): string {
38
+ return relPath.replace(/\.md$/, '')
39
+ }
40
+
41
+ function relPathOf(slug: string): string {
42
+ return `${slug}.md`
43
+ }
44
+
45
+ function defaultSectionFor(relPath: string): string {
46
+ const segments = relPath.split('/')
47
+ return segments.length === 1 ? 'Overview' : (segments[0] ?? 'Overview')
48
+ }
49
+
50
+ function toDoc(entry: DocsCoherenceEntry): Doc {
51
+ const slug = slugOf(entry.relPath)
52
+ const parsed: ParsedDoc = { frontmatter: entry.frontmatter, body: entry.body, firstH1: entry.firstH1 }
53
+ return {
54
+ slug,
55
+ title: deriveTitle(parsed, slug),
56
+ sidebarTitle: entry.frontmatter.sidebarTitle,
57
+ description: entry.frontmatter.description,
58
+ section: entry.frontmatter.section ?? defaultSectionFor(entry.relPath),
59
+ order: entry.frontmatter.order ?? 0,
60
+ href: `/docs/${slug}`,
61
+ filePath: `aeg-root/${entry.relPath}`,
62
+ parentSlug: entry.frontmatter.parent
63
+ }
64
+ }
65
+
66
+ function isReachable(doc: Doc, bySlug: Map<string, Doc>, seen: Set<string> = new Set()): boolean {
67
+ if (!doc.parentSlug) return true
68
+ if (seen.has(doc.slug)) return false // cycle guard
69
+ const parent = bySlug.get(doc.parentSlug)
70
+ if (!parent) return false
71
+ seen.add(doc.slug)
72
+ return isReachable(parent, bySlug, seen)
73
+ }
74
+
75
+ const MD_LINK_PATTERN = /\]\(([^)]+)\)/g
76
+
77
+ /** Resolves `target` relative to the directory of `fromRelPath`. Returns null if it escapes `aeg-root/`. */
78
+ function resolveRelativeMdLink(fromRelPath: string, target: string): string | null {
79
+ const fromDir = fromRelPath.includes('/') ? fromRelPath.slice(0, fromRelPath.lastIndexOf('/')) : ''
80
+ const combined = target.startsWith('/') ? target.slice(1) : fromDir ? `${fromDir}/${target}` : target
81
+
82
+ const stack: string[] = []
83
+ for (const part of combined.split('/')) {
84
+ if (part === '' || part === '.') continue
85
+ if (part === '..') {
86
+ if (stack.length === 0) return null
87
+ stack.pop()
88
+ } else {
89
+ stack.push(part)
90
+ }
91
+ }
92
+ return stack.join('/')
93
+ }
94
+
95
+ export function evaluateDocsCoherence(
96
+ entries: DocsCoherenceEntry[],
97
+ surfacedPaths: ReadonlySet<string> = new Set()
98
+ ): DocsCoherenceResult {
99
+ const errors: string[] = []
100
+ const notes: string[] = []
101
+
102
+ const surfacedEntries = entries.filter((e) => isSurfacedDoc(e.relPath, e.frontmatter, surfacedPaths))
103
+ const surfacedRelPaths = new Set(surfacedEntries.map((e) => e.relPath))
104
+ const docs = surfacedEntries.map(toDoc)
105
+ const bySlug = new Map(docs.map((d) => [d.slug, d]))
106
+
107
+ for (const doc of docs) {
108
+ if (!isReachable(doc, bySlug)) {
109
+ errors.push(`C6: surfaced doc "${relPathOf(doc.slug)}" is not reachable in the doc nav`)
110
+ }
111
+ if (doc.parentSlug && !bySlug.has(doc.parentSlug)) {
112
+ errors.push(
113
+ `C6: nav entry "${relPathOf(doc.slug)}" points at a non-existent/excluded doc "${relPathOf(doc.parentSlug)}"`
114
+ )
115
+ }
116
+ }
117
+
118
+ for (const entry of surfacedEntries) {
119
+ for (const match of entry.body.matchAll(MD_LINK_PATTERN)) {
120
+ const raw = (match[1] ?? '').trim()
121
+ const target = (raw.split('#')[0] ?? '').trim()
122
+ if (!target) continue // pure same-doc anchor, e.g. `(#some-heading)`
123
+ if (/^https?:\/\//.test(target) || target.startsWith('mailto:')) continue
124
+ if (!target.endsWith('.md')) continue
125
+
126
+ const resolved = resolveRelativeMdLink(entry.relPath, target)
127
+ if (resolved === null) continue // escapes aeg-root/ — out of scope for this check
128
+
129
+ if (!surfacedRelPaths.has(resolved)) {
130
+ errors.push(`C6: link "${target}" in "${entry.relPath}" resolves to no surfaced doc`)
131
+ }
132
+ }
133
+ }
134
+
135
+ return { errors, notes }
136
+ }
@@ -0,0 +1,29 @@
1
+ export type { Doc, DocFrontmatter, DocNav, DocSection } from './types'
2
+ export { parseDocFrontmatter, deriveTitle, stripLeadingH1 } from './parse-doc'
3
+ export type { ParsedDoc } from './parse-doc'
4
+ export { buildDocNav } from './build-doc-nav'
5
+ export type { BuildDocNavOptions } from './build-doc-nav'
6
+ export { findDoc, getNextDoc, getPrevDoc } from './nav-helpers'
7
+ export { isSurfacedDoc, surfacedDocs, modelBackedDocPaths } from './surfaced-manifest'
8
+ export type { SurfacedManifestEntry } from './surfaced-manifest'
9
+ export { nodeDocRoute, nodeDocHref } from './node-route'
10
+ export type { NodeDocRoute } from './node-route'
11
+ export { legacyAnchorSlugs } from './legacy-anchors'
12
+ export { evaluateDocsCoherence } from './docs-coherence'
13
+ export type { DocsCoherenceEntry, DocsCoherenceResult } from './docs-coherence'
14
+ export {
15
+ ALLOWED_MECHANICS,
16
+ CONTRACT_BLOCKS,
17
+ countWords,
18
+ enforcementPublishedText,
19
+ evaluatePublishedProse,
20
+ extractShortVersion,
21
+ publishedDoctrineBody,
22
+ readabilityErrors,
23
+ REFERENCE_HEADING,
24
+ ROLE_BLOCKS,
25
+ SHORT_VERSION_HEADING,
26
+ SHORT_VERSION_MAX_WORDS,
27
+ SHORT_VERSION_MIN_WORDS
28
+ } from './published-prose'
29
+ export type { PublishedProseEntry, PublishedProseResult } from './published-prose'
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The old-slug → new-slug alias derivation for `/docs` gate/check anchors.
3
+ * `node-route.ts`'s `nodeSlug()` computes an anchor from a node's display
4
+ * form (G-code stripped, over-length names cut at a clause boundary), which
5
+ * for a handful of nodes now differs from the slug `diagram-model.ts`
6
+ * originally stamped into the node id. A reader who bookmarked, or a page
7
+ * that still links to, the old `#g1-implementation-exists`-style fragment
8
+ * must keep landing on the right section — this is the input to that alias.
9
+ *
10
+ * Derived, not hand-listed: the failure mode this replaces is exactly the
11
+ * one `node-route.ts`'s own header comment warns against for a second slug
12
+ * source. Zero I/O, additive export — takes an already-derived node, returns
13
+ * data, never reads a file (aeg-core purity, #372/#382/#506).
14
+ */
15
+
16
+ import type { DiagramNode } from '../diagram-model'
17
+ import { nodeDocRoute } from './node-route'
18
+
19
+ /**
20
+ * A doctrine cell's own text renamed outright (not a G-code strip, not an
21
+ * over-length cut) is the one case this file cannot derive. `node.id` is
22
+ * `slugify(row.action)` computed fresh from whatever `enforcement.md` says
23
+ * RIGHT NOW — by the time a rename lands, the old text is gone from the
24
+ * doctrine snapshot entirely, so there is nothing left in `node.id`/
25
+ * `node.label` for a derivation to diff against. This is not a second
26
+ * routing authority — `nodeSlug()`/`nodeDocRoute()` still compute every
27
+ * live anchor unconditionally; this map only remembers what one specific
28
+ * anchor used to be before an editor rewrote the cell it came from, and it
29
+ * grows by exactly one entry, once, at the moment of a rename. Keyed by the
30
+ * node id the rename produced. */
31
+ const RENAMED_CELL_ALIASES: Record<string, string> = {
32
+ 'check:coherence-check': 'coherence-oracle'
33
+ }
34
+
35
+ /**
36
+ * The anchor slugs this node used to publish and must continue to answer
37
+ * to. `[]` when the node's canonical slug is unchanged — most nodes, since
38
+ * `nodeSlug()`'s cleanup is a no-op on an already-clean display form.
39
+ */
40
+ export function legacyAnchorSlugs(node: DiagramNode): string[] {
41
+ const rawSlug = node.id.slice(node.kind.length + 1)
42
+ const canonicalSlug = nodeDocRoute(node)?.slug
43
+ const aliases = new Set<string>()
44
+ if (canonicalSlug && canonicalSlug !== rawSlug) aliases.add(rawSlug)
45
+ const renamed = RENAMED_CELL_ALIASES[node.id]
46
+ if (renamed) aliases.add(renamed)
47
+ return [...aliases]
48
+ }
@@ -0,0 +1,17 @@
1
+ import type { Doc, DocNav } from './types'
2
+
3
+ export function findDoc(nav: DocNav, slug: string): Doc | undefined {
4
+ return nav.flat.find((d) => d.slug === slug)
5
+ }
6
+
7
+ export function getNextDoc(nav: DocNav, slug: string): Doc | undefined {
8
+ const idx = nav.flat.findIndex((d) => d.slug === slug)
9
+ if (idx === -1 || idx === nav.flat.length - 1) return undefined
10
+ return nav.flat[idx + 1]
11
+ }
12
+
13
+ export function getPrevDoc(nav: DocNav, slug: string): Doc | undefined {
14
+ const idx = nav.flat.findIndex((d) => d.slug === slug)
15
+ if (idx <= 0) return undefined
16
+ return nav.flat[idx - 1]
17
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The single node → `/docs` route derivation. Nav construction
3
+ * (`load-aeg-docs.ts`), the "Read more" resolver (`read-more.ts`), and the
4
+ * harness map (`/docs` `page.tsx`) all resolve a `DiagramNode` to its docs
5
+ * location through THIS function — one source of truth, so a card's deep-link,
6
+ * a "Read more" anchor, and a ring/action page's heading `id` can never point
7
+ * at three different places for the same node. A parallel, hand-maintained
8
+ * slug list is the exact failure mode this replaces (the same discipline
9
+ * `surfaced-manifest.ts` holds for the file allowlist).
10
+ *
11
+ * Granularity follows content size. A role or contract is a whole
12
+ * `aeg-root/**.md` file, so it keeps its own page and needs no anchor. A gate
13
+ * or check is one row of `enforcement.md`, and an action is one entry of
14
+ * `ACTIONS`, so each is an `#`-anchored section inside a grouping page
15
+ * (`/docs/rings/ring-<n>` or `/docs/actions`).
16
+ *
17
+ * The anchor slug is recomputed here from the node's DISPLAY form, not read
18
+ * verbatim off the node id. `node.id`/`node.label` keep the full doctrine
19
+ * text — G-codes included — because the registry check (G1-G5,
20
+ * `registry-checks.ts`) matches on that exact id; a slug that a reader
21
+ * follows into a URL fragment has no such constraint, so it is cleaned at
22
+ * this one edge instead. Both the heading (`humanLabel`/`shortLabel`, in
23
+ * `apps/vinaya/web`) and this anchor apply the same two rules — strip a
24
+ * leading G-code, cut a pathologically long name at its first clause — so a
25
+ * heading and its anchor still agree, just no longer byte-identical to the
26
+ * doctrine id. A node whose display form needs no cleanup still gets the
27
+ * same slug it always did, since stripping/cutting are no-ops on it.
28
+ *
29
+ * Zero I/O, additive export — takes an already-derived node, returns data,
30
+ * never reads a file (aeg-core purity, #372/#382/#506).
31
+ */
32
+
33
+ import type { DiagramNode } from '../diagram-model'
34
+
35
+ export type NodeDocRoute = {
36
+ /** The page a node lives on. */
37
+ route: string
38
+ /** The heading anchor within that page, or `null` for a file-sized node
39
+ * (role/contract) that owns its whole page. */
40
+ slug: string | null
41
+ }
42
+
43
+ /** A pathologically long anchor is cut at its first clause boundary rather
44
+ * than published whole. 100 is chosen against the current doctrine, not a
45
+ * round guess: the longest anchor slug outside the one row this exists to
46
+ * fix is 84 characters (a ring-0 gate's compound name), so 100 leaves that
47
+ * row untouched while still catching genuinely oversized names — the one
48
+ * 127-character row this was written for included. */
49
+ const MAX_ANCHOR_SLUG_LENGTH = 100
50
+
51
+ /** Strips a leading `g<n>-` code off an already-slugified anchor. Operates on
52
+ * the slug form (not `node.label`) so a node whose id isn't `slugify(label)`
53
+ * to begin with — an `action`, whose id is the hand-authored `ACTIONS` entry
54
+ * id, not a re-derivation of its label — is never touched: no G-coded or
55
+ * over-length action exists today, so this function is a no-op for every one
56
+ * of them, exactly preserving their existing anchors. */
57
+ function stripGCodeSlug(slug: string): string {
58
+ return slug.replace(/^g\d+-/, '')
59
+ }
60
+
61
+ /** Strips the same `G<n> — ` doctrine code off raw label text, for the
62
+ * over-length fallback below. Same contract as `humanLabel` in
63
+ * `apps/vinaya/web`'s `display-label.ts` (duplicated rather than shared:
64
+ * aeg-core cannot import from the web app, and this rule is small enough
65
+ * that keeping two copies in sync by inspection is cheaper than a new
66
+ * cross-package export). */
67
+ function stripGCodeLabel(label: string): string {
68
+ return label.replace(/^G\d+\s*—\s*/, '')
69
+ }
70
+
71
+ /** Cuts at the first clause boundary, same separator set `shortLabel` uses
72
+ * in `display-label.ts` — kept in sync with that function's comment by
73
+ * inspection, for the same reason `stripGCodeLabel` is duplicated above. */
74
+ function firstClause(label: string): string {
75
+ return label.split(/ \(| \/ |—|–|: /)[0]?.trim() ?? label
76
+ }
77
+
78
+ /** Same cleanup `diagram-model.ts`'s own (private, un-exported) `slugify`
79
+ * applies when it stamps a node id — duplicated here rather than imported so
80
+ * this file never needs `diagram-model.ts` to export it. Reached only by the
81
+ * over-length fallback below, never by the common case. */
82
+ function slugifyText(text: string): string {
83
+ return text
84
+ .toLowerCase()
85
+ .replace(/`/g, '')
86
+ .replace(/[^a-z0-9]+/g, '-')
87
+ .replace(/^-+|-+$/g, '')
88
+ }
89
+
90
+ /** The anchor slug for a node's display form. Strips a G-code off the
91
+ * existing id-stamped slug first — a byte-preserving operation for every
92
+ * node that never had one. Only if the result is STILL too long to publish
93
+ * does this fall back to re-deriving a short slug from `node.label`'s first
94
+ * clause; that fallback is the one path that can produce a slug other than
95
+ * a prefix of the id-stamped one, and today only one row (a 127-character
96
+ * ring-0 gate name) reaches it. */
97
+ function nodeSlug(node: DiagramNode): string {
98
+ const rawSlug = node.id.slice(node.kind.length + 1)
99
+ const stripped = stripGCodeSlug(rawSlug)
100
+ if (stripped.length <= MAX_ANCHOR_SLUG_LENGTH) return stripped
101
+ return slugifyText(firstClause(stripGCodeLabel(node.label)))
102
+ }
103
+
104
+ /**
105
+ * Resolve a node to its `/docs` route + anchor slug. Returns `null` for a kind
106
+ * with no docs surface (there is none today — every kind resolves).
107
+ */
108
+ export function nodeDocRoute(node: DiagramNode): NodeDocRoute | null {
109
+ switch (node.kind) {
110
+ case 'role':
111
+ return { route: `/docs/roles/${node.label}`, slug: null }
112
+ case 'contract':
113
+ return { route: `/docs/contracts/${node.label}`, slug: null }
114
+ case 'gate':
115
+ case 'check':
116
+ if (node.ringIndex === undefined) return null
117
+ return { route: `/docs/rings/ring-${node.ringIndex}`, slug: nodeSlug(node) }
118
+ case 'action':
119
+ return { route: '/docs/actions', slug: nodeSlug(node) }
120
+ case 'ring':
121
+ if (node.ringIndex === undefined) return null
122
+ return { route: `/docs/rings/ring-${node.ringIndex}`, slug: null }
123
+ default:
124
+ return null
125
+ }
126
+ }
127
+
128
+ /** The full in-app href — `route` plus a `#slug` fragment when the node is an
129
+ * anchored section. The form the map and `read-more.ts` link to directly. */
130
+ export function nodeDocHref(node: DiagramNode): string | null {
131
+ const resolved = nodeDocRoute(node)
132
+ if (!resolved) return null
133
+ return resolved.slug ? `${resolved.route}#${resolved.slug}` : resolved.route
134
+ }
@@ -0,0 +1,43 @@
1
+ import matter from 'gray-matter'
2
+ import type { DocFrontmatter } from './types'
3
+
4
+ export type ParsedDoc = {
5
+ frontmatter: DocFrontmatter
6
+ body: string
7
+ firstH1?: string
8
+ }
9
+
10
+ const H1_PATTERN = /^\s*#\s+(.+?)\s*$/m
11
+
12
+ export function parseDocFrontmatter(raw: string): ParsedDoc {
13
+ const { data, content } = matter(raw)
14
+ const fm = data as Record<string, unknown>
15
+
16
+ const frontmatter: DocFrontmatter = {}
17
+ if (typeof fm.title === 'string') frontmatter.title = fm.title
18
+ if (typeof fm.sidebarTitle === 'string') {
19
+ frontmatter.sidebarTitle = fm.sidebarTitle
20
+ } else if (typeof fm.sidebar_title === 'string') {
21
+ frontmatter.sidebarTitle = fm.sidebar_title
22
+ }
23
+ if (typeof fm.description === 'string') frontmatter.description = fm.description
24
+ if (typeof fm.section === 'string') frontmatter.section = fm.section
25
+ if (typeof fm.order === 'number') frontmatter.order = fm.order
26
+ if (typeof fm.parent === 'string') frontmatter.parent = fm.parent
27
+ if (typeof fm.surfaced === 'boolean') frontmatter.surfaced = fm.surfaced
28
+
29
+ const h1Match = content.match(H1_PATTERN)
30
+ const firstH1 = h1Match?.[1]?.trim()
31
+
32
+ return { frontmatter, body: content, firstH1 }
33
+ }
34
+
35
+ export function deriveTitle(parsed: ParsedDoc, fallbackFromPath: string): string {
36
+ if (parsed.frontmatter.title) return parsed.frontmatter.title
37
+ if (parsed.firstH1) return parsed.firstH1
38
+ return fallbackFromPath
39
+ }
40
+
41
+ export function stripLeadingH1(body: string): string {
42
+ return body.replace(/^\s*#\s+.*\n+/, '')
43
+ }