@attalabs/vinaya 0.22.0 → 0.24.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 (194) hide show
  1. package/README.md +14 -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 +559 -140
  23. package/dist/checks/bin/check-branch-topology.js +630 -145
  24. package/dist/checks/bin/check-brief-shape.js +600 -143
  25. package/dist/checks/bin/check-changeset-coverage.js +942 -162
  26. package/dist/checks/bin/check-closes-n.js +630 -145
  27. package/dist/checks/bin/check-coherence.js +641 -148
  28. package/dist/checks/bin/check-dead-branch-push.js +559 -140
  29. package/dist/checks/bin/check-dispatch-readiness.js +639 -146
  30. package/dist/checks/bin/check-doc-coverage-push.js +696 -219
  31. package/dist/checks/bin/check-doc-coverage.js +696 -219
  32. package/dist/checks/bin/check-doctrine-no-procedures.js +4800 -0
  33. package/dist/checks/bin/check-doctrine-portability.js +691 -147
  34. package/dist/checks/bin/check-evidence-fresh.js +1048 -222
  35. package/dist/checks/bin/check-exec-bits.js +4937 -0
  36. package/dist/checks/bin/check-first-push-dispatch.js +630 -145
  37. package/dist/checks/bin/check-issue-assignment.js +630 -145
  38. package/dist/checks/bin/check-main-branch-refusal.js +565 -141
  39. package/dist/checks/bin/check-no-disk-state.js +559 -140
  40. package/dist/checks/bin/check-pr-report-density.js +4467 -0
  41. package/dist/checks/bin/check-quoted-command.js +656 -164
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +652 -163
  43. package/dist/checks/bin/check-registry-gates.js +602 -145
  44. package/dist/checks/bin/check-retired-vocabulary.js +652 -163
  45. package/dist/checks/bin/check-review-gate.js +677 -142
  46. package/dist/checks/bin/check-single-plan-pr.js +559 -140
  47. package/dist/checks/bin/check-test-plan.js +560 -141
  48. package/dist/checks/bin/check-token-collection-wired.js +559 -140
  49. package/dist/checks/bin/check-token-report.js +559 -140
  50. package/dist/checks/bin/check-workspace-escape.js +992 -146
  51. package/dist/index.js +8884 -5627
  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 +118 -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 +198 -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-render.ts +462 -0
  60. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +912 -0
  61. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
  62. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +806 -0
  63. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/consumer-enumeration.ts +75 -0
  64. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
  65. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
  66. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
  67. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
  68. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
  69. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
  70. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +268 -0
  71. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
  72. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
  73. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
  74. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
  75. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
  76. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
  77. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
  78. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
  79. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
  80. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
  81. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
  82. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-no-procedures.ts +94 -0
  83. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +293 -0
  84. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
  85. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
  86. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
  87. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
  88. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +171 -0
  89. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +323 -0
  90. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
  91. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
  92. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
  93. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
  94. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
  95. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
  96. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
  97. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
  98. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
  99. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
  100. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
  101. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
  102. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +177 -0
  103. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
  104. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-report-density.ts +116 -0
  105. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
  106. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +160 -0
  107. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
  108. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
  109. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
  110. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
  111. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
  112. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
  113. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
  114. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +378 -0
  115. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-status.ts +200 -0
  116. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
  117. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
  118. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
  119. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
  120. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
  121. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
  122. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +117 -0
  123. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
  124. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
  125. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +199 -0
  126. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
  127. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
  128. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
  129. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  131. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  132. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  133. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  143. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  144. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  145. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  147. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  148. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  149. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  150. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  151. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  152. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  153. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  154. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  155. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  156. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  157. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  158. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  159. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  160. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  161. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  162. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  163. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  164. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  165. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  166. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  167. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  168. 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
  169. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  170. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  171. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  172. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  173. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  174. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1pndh3_._.js} +1 -1
  175. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  176. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  177. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  178. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  179. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  180. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  181. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +3 -3
  182. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  183. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  184. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  185. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  186. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  187. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0zebsmmk1bvnb.js} +1 -1
  188. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 1uogpj5w2n5ju.js} +1 -1
  189. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 3m1kgax7j2vgs.js} +1 -1
  190. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  191. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  192. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_buildManifest.js +0 -0
  193. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_clientMiddlewareManifest.js +0 -0
  194. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_ssgManifest.js +0 -0
@@ -0,0 +1,183 @@
1
+ import { readMarkdownTable } from './parse-registry'
2
+ import type { Tranche, Lifecycle, Task } from './types'
3
+
4
+ /**
5
+ * Parse `aeg-root/tranches/<name>.md` into a typed `Tranche`.
6
+ *
7
+ * Captures:
8
+ * - the tranche's name (from `# Tranche: <name> — <timeframe>`; the
9
+ * superseded `# Iteration:` spelling is still read, see `parseName`)
10
+ * - the lifecycle marker (`Lifecycle: active|complete`, defaulting to
11
+ * `'active'` when absent — pre-§11 files have no marker)
12
+ * - the first goal paragraph
13
+ * - the `## Tasks (topology)` table rows
14
+ * - per-task rationale blocks (raw markdown, captured verbatim)
15
+ * - an optional `## Backlog` section as bullets
16
+ *
17
+ * Real tranche files include narrative references to dropped task ids in
18
+ * prose (e.g. a "Task 3a — removed" note). The parser ignores those — only
19
+ * rows present in the topology table become tasks.
20
+ */
21
+ export function parseTranche(md: string): Tranche {
22
+ return {
23
+ name: parseName(md),
24
+ lifecycle: parseLifecycle(md),
25
+ goal: parseGoal(md),
26
+ tasks: parseTasks(md),
27
+ backlog: parseBacklog(md)
28
+ }
29
+ }
30
+
31
+ function parseName(md: string): string {
32
+ // The H1 is `# Tranche: <slug> — <timeframe>`. The slug may contain
33
+ // hyphens (e.g. `some-tranche-name`, `another-v1`), so capture the first
34
+ // non-whitespace run — the space before the em-dash is the delimiter.
35
+ //
36
+ // `Iteration:` is accepted as the superseded spelling, permanently. This is
37
+ // a READER of files this repo does not necessarily own: content pulled out
38
+ // of git history predates the rename and cannot be rewritten, and an
39
+ // adopter's own topology files are theirs, not ours to migrate. Refusing
40
+ // the old marker would not fail loudly — `parseName` returns `''`, which
41
+ // reads downstream as a nameless tranche. The writer side is unaffected:
42
+ // every file this repo emits or archives carries `# Tranche:`.
43
+ const m = md.match(/^#\s+(?:Tranche|Iteration):\s+(\S+)/m)
44
+ return m?.[1] ? m[1].trim() : ''
45
+ }
46
+
47
+ function parseLifecycle(md: string): Lifecycle {
48
+ // `Lifecycle: active` or `Lifecycle: complete`. The label may be wrapped in
49
+ // bold (`**Lifecycle:** active`); the closing `**` sits after the colon.
50
+ const m = md.match(/^\s*(?:\*\*)?Lifecycle\s*:(?:\*\*)?\s*(active|complete)\b/im)
51
+ return m?.[1] ? (m[1].toLowerCase() as Lifecycle) : 'active'
52
+ }
53
+
54
+ function parseGoal(md: string): string {
55
+ // Match `Goal (…): <text>` to the end of the paragraph.
56
+ // The live files use various subtitles ("execution, not roadmap-why",
57
+ // "execution, not product-why"); strip everything up to the colon and
58
+ // capture the first paragraph that follows.
59
+ const lines = md.split(/\r?\n/)
60
+ const buf: string[] = []
61
+ let inGoal = false
62
+ for (const raw of lines) {
63
+ const line = stripBoldWrappers(raw)
64
+ if (!inGoal) {
65
+ const m = line.match(/^Goal\b[^:]*:\s*(.*)$/i)
66
+ if (m) {
67
+ inGoal = true
68
+ const rest = (m[1] ?? '').trim()
69
+ if (rest) buf.push(rest)
70
+ }
71
+ } else {
72
+ if (line.trim() === '') break
73
+ // Stop at the next markdown heading or HR.
74
+ if (/^#{1,6}\s/.test(line) || /^---+\s*$/.test(line)) break
75
+ buf.push(line.trim())
76
+ }
77
+ }
78
+ return buf.join(' ').trim()
79
+ }
80
+
81
+ function stripBoldWrappers(line: string): string {
82
+ // Remove bold markers around `Goal …:` or `Lifecycle:` style lines so the
83
+ // greedy regexes above work on either `**Goal:**` or plain `Goal:`.
84
+ return line.replace(/\*\*/g, '')
85
+ }
86
+
87
+ function parseTasks(md: string): Task[] {
88
+ const rows = readMarkdownTable(md, /^##\s+Tasks\b/i)
89
+ const rationales = extractRationales(md)
90
+ const tasks: Task[] = []
91
+ for (const row of rows) {
92
+ // Expected 6 columns: # | Task | Issue | Project(s) | Depends-on | Conflicts-with.
93
+ if (row.length < 6) continue
94
+ const id = (row[0] ?? '').trim()
95
+ if (!id) continue
96
+ tasks.push({
97
+ id,
98
+ title: (row[1] ?? '').trim(),
99
+ issue: parseIssueCell(row[2] ?? ''),
100
+ projects: parseCsvCell(row[3] ?? ''),
101
+ dependsOn: parseEdgeCell(row[4] ?? ''),
102
+ conflictsWith: parseEdgeCell(row[5] ?? ''),
103
+ rationaleMarkdown: rationales.get(id) ?? ''
104
+ })
105
+ }
106
+ return tasks
107
+ }
108
+
109
+ function parseIssueCell(cell: string): number | null {
110
+ const trimmed = stripBackticks(cell).trim()
111
+ if (!trimmed) return null
112
+ if (isEmDashOrDash(trimmed)) return null
113
+ const m = trimmed.match(/#?(\d+)/)
114
+ return m ? Number(m[1]) : null
115
+ }
116
+
117
+ function parseCsvCell(cell: string): string[] {
118
+ const cleaned = stripBackticks(cell).trim()
119
+ if (!cleaned || isEmDashOrDash(cleaned)) return []
120
+ return cleaned
121
+ .split(',')
122
+ .map((s) => s.trim())
123
+ .filter(Boolean)
124
+ }
125
+
126
+ function parseEdgeCell(cell: string): string[] {
127
+ return parseCsvCell(cell)
128
+ }
129
+
130
+ function isEmDashOrDash(s: string): boolean {
131
+ const t = s.trim()
132
+ return t === '—' || t === '-' || t === '–'
133
+ }
134
+
135
+ function stripBackticks(s: string): string {
136
+ return s.replace(/`/g, '')
137
+ }
138
+
139
+ /**
140
+ * Capture each `### Task <id> — …` block as raw markdown — from the heading
141
+ * through the line before the next `### ` / `## ` / `# ` / end-of-file.
142
+ * The id may be `1`, `7a`, `7b`, etc. Per-task rationale lives inline in the
143
+ * tranche file (see §4 template); the planner's rationale also lives in the
144
+ * Issue body, but the inline copy is what `parseTranche` exposes.
145
+ */
146
+ function extractRationales(md: string): Map<string, string> {
147
+ const out = new Map<string, string>()
148
+ const lines = md.split(/\r?\n/)
149
+ let i = 0
150
+ while (i < lines.length) {
151
+ const m = (lines[i] ?? '').match(/^###\s+Task\s+(\S+)\s+—/)
152
+ if (!m?.[1]) {
153
+ i++
154
+ continue
155
+ }
156
+ const id = m[1]
157
+ const start = i
158
+ i++
159
+ while (i < lines.length && !/^#{1,3}\s/.test(lines[i] ?? '')) i++
160
+ out.set(id, lines.slice(start, i).join('\n').trimEnd())
161
+ }
162
+ return out
163
+ }
164
+
165
+ function parseBacklog(md: string): string[] {
166
+ const lines = md.split(/\r?\n/)
167
+ let i = 0
168
+ for (; i < lines.length; i++) {
169
+ if (/^##\s+Backlog\b/i.test(lines[i] ?? '')) {
170
+ i++
171
+ break
172
+ }
173
+ }
174
+ if (i >= lines.length) return []
175
+ const bullets: string[] = []
176
+ for (; i < lines.length; i++) {
177
+ const line = lines[i] ?? ''
178
+ if (/^#{1,3}\s/.test(line)) break
179
+ const m = line.match(/^\s*[-*+]\s+(.*)$/)
180
+ if (m?.[1]) bullets.push(m[1].trim())
181
+ }
182
+ return bullets
183
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * PR-report density (aeg-governance-hardening, dogfooding finding 2026-09-02).
3
+ * Pure — no `fs`, no `fetch`, no `process.env`.
4
+ *
5
+ * `aeg-root/roles/developer.md` § "PR body — canonical form" and
6
+ * `aeg-root/templates/pr-report-template.md` both require `## Summary` and
7
+ * `## Scope` to be exactly one paragraph — narrative root-cause detail
8
+ * belongs in a section of its own below the canonical four, a commit
9
+ * message, or the changeset, never padded into the report itself and never
10
+ * hand-typed into the AEG:EVIDENCE block. Nothing enforced that rule
11
+ * mechanically:
12
+ * confirmed live, three real PRs (`atta-labs/vinaya#343`, `#346`, `#354`)
13
+ * shipped multi-paragraph Summary/Scope sections and nothing caught it,
14
+ * neither at `vinaya pr create`/`pr edit` time nor in CI. Longer, denser
15
+ * prose is also strictly more surface for every other body-text gate
16
+ * (`body-bare-digits`, `reader-resolvable-prose`'s glossary rule) — the same
17
+ * class of gap `#341` hit in code, just in prose instead.
18
+ *
19
+ * Deterministic and structural only, matching `brief-validation.ts`'s own
20
+ * "presence-only" philosophy: this counts blank-line-delimited text blocks,
21
+ * never judges whether the prose itself is good.
22
+ *
23
+ * The rule is literal — one paragraph, full stop — so a `### subsection`, a
24
+ * bullet list, or a markdown table under Summary/Scope all fail too, same as
25
+ * a second prose paragraph would (confirmed by probe, PR review on #358).
26
+ * That is intentional, not an oversight: `developer.md`'s own escape hatch
27
+ * for exactly this case is "Add anything you want beneath the four
28
+ * sections" — structured detail belongs in a section of its own below
29
+ * Summary/Scope/Test-plan/Evidence, not folded into one of the four. A
30
+ * multi-line blockquote is the one shape this rule does NOT split on (its
31
+ * `\n>\n` continuation lines are never blank) — but only when the
32
+ * blockquote IS the section, start to finish: prose before or after it is
33
+ * still a separate blank-line-delimited block, so "intro sentence, then a
34
+ * blockquote aside, then a closing sentence" is three blocks and still
35
+ * fails (confirmed by probe, PR review on #358 — this is not the
36
+ * "structured aside alongside prose" shape it can look like at a glance).
37
+ * The exemption only covers a section whose entire content, quoted in
38
+ * full, is the one thing being said.
39
+ */
40
+
41
+ import { anchoredRegionBounds, stripCode } from './anchored-region'
42
+
43
+ export type DensityResult = { status: 'pass' | 'fail'; errors: string[] }
44
+
45
+ /**
46
+ * The named `## <heading>` section's raw body text — from just after the
47
+ * heading line to the next `##`-or-shallower heading, or end of body.
48
+ * `null` when the heading itself is absent (a different check's job to
49
+ * require it — this one is silent on presence, per the module's scope).
50
+ */
51
+ function headingSectionBody(prBody: string, heading: string): string | null {
52
+ const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, 'im')
53
+ const m = headingRe.exec(prBody)
54
+ if (!m) return null
55
+ const start = m.index + m[0].length
56
+ const rest = prBody.slice(start)
57
+ const nextHeading = /^#{1,2}\s/m.exec(rest)
58
+ return nextHeading ? rest.slice(0, nextHeading.index) : rest
59
+ }
60
+
61
+ /**
62
+ * `headingSectionBody`'s text with every well-formed `AEG:*` anchor block removed
63
+ * outright (not just unwrapped) — an anchored field (Scope's trailing
64
+ * `AEG:TIER` block, in the canonical template) is a field, not a second
65
+ * prose paragraph, and must not count as one. Reuses `anchoredRegionBounds`
66
+ * (the same span `vinaya pr report --write` targets) rather than a second,
67
+ * drifting regex for "what an anchor block looks like."
68
+ */
69
+ function stripAnchorBlocks(sectionText: string): string {
70
+ let result = sectionText
71
+ for (const field of ['CLOSES', 'PROJECT', 'TIER', 'PREMISE', 'TEST-PLAN', 'EVIDENCE'] as const) {
72
+ for (;;) {
73
+ const bounds = anchoredRegionBounds(result, field)
74
+ if (!bounds) break
75
+ result = result.slice(0, bounds.outerStart) + result.slice(bounds.outerEnd)
76
+ }
77
+ }
78
+ return result
79
+ }
80
+
81
+ /** Non-empty, blank-line-delimited text blocks, code-blind (a fenced example inside the section is not a second paragraph). */
82
+ function paragraphCount(sectionText: string): number {
83
+ const stripped = stripCode(stripAnchorBlocks(sectionText)).trim()
84
+ if (stripped.length === 0) return 0
85
+ return stripped
86
+ .split(/\n[ \t]*\n/)
87
+ .map((block) => block.trim())
88
+ .filter((block) => block.length > 0).length
89
+ }
90
+
91
+ function checkSectionDensity(prBody: string, heading: string): DensityResult {
92
+ const body = headingSectionBody(prBody, heading)
93
+ if (body === null) return { status: 'pass', errors: [] }
94
+ const count = paragraphCount(body)
95
+ if (count <= 1) return { status: 'pass', errors: [] }
96
+ return {
97
+ status: 'fail',
98
+ errors: [
99
+ `pr-report-density ${heading}: "## ${heading}" holds ${count} paragraphs — the canonical PR-report form (aeg-root/roles/developer.md § PR body) requires exactly one. Collapse it to one paragraph; move the rest into a section of your own below the canonical four ("Add anything you want beneath the four sections", developer.md), a commit message, or the changeset — never into the AEG:EVIDENCE block, which is emitted only, never hand-typed.`
100
+ ]
101
+ }
102
+ }
103
+
104
+ export function checkSummaryDensity(prBody: string): DensityResult {
105
+ return checkSectionDensity(prBody, 'Summary')
106
+ }
107
+
108
+ export function checkScopeDensity(prBody: string): DensityResult {
109
+ return checkSectionDensity(prBody, 'Scope')
110
+ }
111
+
112
+ /** Aggregates both section checks — one error line per over-dense section. */
113
+ export function checkPrReportDensity(prBody: string): { errors: string[] } {
114
+ const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)]
115
+ return { errors: results.flatMap((r) => r.errors) }
116
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * PR tier derivation and the docs-gate override check. Pure — takes the diff,
3
+ * PR body, and env-derived strings as explicit parameters. The original
4
+ * `scripts/verify-docs.ts` read `process.env.PR_BODY` / `OVERRIDE_DOCS` /
5
+ * `PR_LABELS` directly inside these functions; that hidden I/O is removed
6
+ * here — the caller (the CLI shim) reads env once and passes the values in.
7
+ */
8
+
9
+ import { hasLabel, label } from '@attalabs/aeg-forge-state'
10
+ import { anchoredRegion } from './anchored-region'
11
+ import { isDocFile, isSpecFile } from './file-classify'
12
+
13
+ /**
14
+ * Derive a tier from the changed-file list when no `Tier:` field is in the PR body.
15
+ *
16
+ * Rules (in priority order):
17
+ * 1. Spec or doc file in diff → Tier 1
18
+ * 2. Otherwise (code/config…) → Tier 0
19
+ *
20
+ * A frozen archive carries no tier signal: nothing reads it, so touching one
21
+ * says nothing about a change's impact.
22
+ */
23
+ export function deriveTierFromDiff(changed: string[]): 0 | 1 {
24
+ if (changed.some((p) => isSpecFile(p) || isDocFile(p))) return 1
25
+ return 0
26
+ }
27
+
28
+ /**
29
+ * Read the `Tier:` field from the PR body.
30
+ *
31
+ * Tolerates the three markdown shapes the field appears in:
32
+ * - plain: `Tier: 3`
33
+ * - bold colon: `**Tier:** 3` (the `**` wraps `Tier:` including the colon)
34
+ * - bold label: `**Tier**: 3` (the `**` wraps only `Tier`)
35
+ *
36
+ * The field may appear inline in a metadata line (e.g.
37
+ * `Tranche: x · Task: 1 · **Tier:** 3 · Project: y`), so it is NOT anchored
38
+ * to line-start. Returns null when no Tier field is present at all — the caller
39
+ * decides what a missing tier means (PR mode treats it as an explicit error,
40
+ * NOT a silent default).
41
+ *
42
+ * When the body carries an `AEG:TIER` anchor pair (`anchored-region.ts`,
43
+ * task 30), the same regex runs exclusively inside that pair — a `Tier:`
44
+ * mention anywhere else (a pasted reference brief, a quoted example) is
45
+ * ignored. Bodies without the pair parse exactly as before.
46
+ */
47
+ /**
48
+ * The exact `Tier:` field grammar, exported so a consumer that needs the
49
+ * MATCH itself (not just the parsed number) — `body-bare-digits`, which
50
+ * needs to know precisely which substring is the field's real value so it
51
+ * can exempt exactly that and nothing appended after it — reuses this one
52
+ * definition rather than a second regex that could silently drift from it.
53
+ * An optional bold-open, the word Tier, an optional bold-close, a colon, an
54
+ * optional bold-close (covers `**Tier:**`), optional space, then the digit.
55
+ */
56
+ export const TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i
57
+
58
+ export function readTierFromPrBody(prBody: string): 0 | 1 | 3 | null {
59
+ const searchIn = anchoredRegion(prBody, 'TIER') ?? prBody
60
+ const m = searchIn.match(TIER_FIELD)
61
+ if (!m) return null
62
+ const t = Number(m[4])
63
+ return t === 0 || t === 1 || t === 3 ? (t as 0 | 1 | 3) : null
64
+ }
65
+
66
+ /**
67
+ * The body token that activates the override — the label name in brackets, so
68
+ * the two spellings cannot drift. Built from the code-owned vocabulary rather
69
+ * than written as a literal (#614): `override:docs` was the one §14 system
70
+ * label the namespace migration missed, precisely because it lived here as a
71
+ * bare string that no label-family grep looked for.
72
+ */
73
+ const OVERRIDE_BODY_TOKEN = `[${label('override-docs')}]`
74
+
75
+ export function overrideActive(opts: { overrideDocsEnv?: string; prLabels?: string; prBody?: string }): boolean {
76
+ if (opts.overrideDocsEnv === '1') return true
77
+ const labels = (opts.prLabels || '').split(',').map((s) => s.trim())
78
+ if (hasLabel('override-docs', labels)) return true
79
+ if ((opts.prBody || '').includes(OVERRIDE_BODY_TOKEN)) return true
80
+ return false
81
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Premise-pinning grammar (aeg-governance-hardening task 11, #324). Pure —
3
+ * no `fs`; file content is injected via `fileReader` so the checker stays
4
+ * testable and CWD-independent.
5
+ *
6
+ * A brief/PR body's `Premise:` block pins concrete, checkable facts about the
7
+ * code surface at authoring time — e.g. "this function still exists", "this
8
+ * constant is still absent". `verify-dispatch --premise <body-file>`
9
+ * re-asserts every pin immediately before Step 0: a premise that no longer
10
+ * holds means the surface moved since the brief was written, and the
11
+ * Developer should stop and re-dig rather than execute against a stale
12
+ * mental model (the exact failure of aeg-governance-hardening's live-fire
13
+ * #3 — two briefs described a target architecture a later, uncited migration
14
+ * had already superseded).
15
+ *
16
+ * Deliberately minimal — exactly three assertion kinds. This is a pin
17
+ * format, not a DSL; resist adding more.
18
+ */
19
+
20
+ import { createHash } from 'node:crypto'
21
+ import { anchoredRegion } from './anchored-region'
22
+
23
+ export type PremiseAssertion =
24
+ | { kind: 'contains'; path: string; value: string }
25
+ | { kind: 'absent'; path: string; value: string }
26
+ | { kind: 'sha256'; path: string; value: string }
27
+
28
+ const ASSERTION_KINDS = new Set(['contains', 'absent', 'sha256'])
29
+
30
+ /**
31
+ * True for the header line in either serialization the rest of the brief
32
+ * grammar uses (`brief-validation.ts`, `issue-validation.ts`): `**Premise:**`
33
+ * bold-inline or `### Premise` heading. Strips `*`/`#` markup first rather
34
+ * than trying to enumerate every marker placement — `**Premise:**` closes
35
+ * its bold markers *after* the colon, `**Premise**:` closes them *before*.
36
+ */
37
+ function isPremiseHeader(line: string): boolean {
38
+ const stripped = line.replace(/[*#]/g, '').trim()
39
+ return /^premise\s*:?$/i.test(stripped)
40
+ }
41
+
42
+ /** Matches one bullet line: `- <path> <kind>: <value>`. */
43
+ const PREMISE_LINE = /^[-*]\s*(\S+)\s+(contains|absent|sha256)\s*:\s*(.+)$/i
44
+
45
+ /**
46
+ * Parse every `- <path> <kind>: <value>` bullet under the `Premise:` header.
47
+ * Malformed bullets (wrong kind, missing colon) are silently skipped — this
48
+ * mirrors `parsePremiseBlock`'s sibling parsers' presence-only philosophy;
49
+ * the caller decides whether zero assertions is itself a failure
50
+ * (`checkPremiseCoverage`). Stops the block at the first blank line or the
51
+ * first non-bullet line after the header.
52
+ *
53
+ * When the body carries an `AEG:PREMISE` anchor pair (`anchored-region.ts`,
54
+ * task 30), the block is parsed exclusively inside that pair — the pair must
55
+ * contain the `**Premise:**` header line and its bullets; a premise-shaped
56
+ * block anywhere else in the body is ignored. Bodies without the pair parse
57
+ * exactly as before.
58
+ */
59
+ export function parsePremiseBlock(prBody: string): PremiseAssertion[] {
60
+ const searchIn = anchoredRegion(prBody, 'PREMISE') ?? prBody
61
+ const lines = searchIn.split(/\r?\n/)
62
+ const assertions: PremiseAssertion[] = []
63
+ let inBlock = false
64
+
65
+ for (const raw of lines) {
66
+ const line = raw.trim()
67
+ if (!inBlock) {
68
+ if (isPremiseHeader(line)) inBlock = true
69
+ continue
70
+ }
71
+ if (line === '') break
72
+ const m = line.match(PREMISE_LINE)
73
+ if (!m) break
74
+ const [, path, kindRaw, value] = m
75
+ const kind = (kindRaw as string).toLowerCase()
76
+ if (!ASSERTION_KINDS.has(kind)) continue
77
+ assertions.push({ kind, path: path as string, value: (value as string).trim() } as PremiseAssertion)
78
+ }
79
+
80
+ return assertions
81
+ }
82
+
83
+ /**
84
+ * The `Premise:` block's own raw text (header line through its last bullet),
85
+ * mirroring `parsePremiseBlock`'s exact same header/bullet/stop rules so the
86
+ * two can never disagree about where the block ends. Returns `null` when no
87
+ * header is found. Used by `brief-validation.ts`'s `checkNoUnpinnedCodeClaims`
88
+ * to exclude the block's own bullets (a pin's path is a legitimate file
89
+ * reference, not an unpinned prose claim) — reuses this parser's block
90
+ * boundaries rather than a second one.
91
+ */
92
+ export function premiseBlockText(prBody: string): string | null {
93
+ const searchIn = anchoredRegion(prBody, 'PREMISE') ?? prBody
94
+ const lines = searchIn.split(/\r?\n/)
95
+ let startIdx = -1
96
+ let endIdxExclusive = lines.length
97
+
98
+ for (let i = 0; i < lines.length; i++) {
99
+ const line = (lines[i] as string).trim()
100
+ if (startIdx === -1) {
101
+ if (isPremiseHeader(line)) startIdx = i
102
+ continue
103
+ }
104
+ if (line === '') {
105
+ endIdxExclusive = i
106
+ break
107
+ }
108
+ if (!PREMISE_LINE.test(line)) {
109
+ endIdxExclusive = i
110
+ break
111
+ }
112
+ }
113
+
114
+ if (startIdx === -1) return null
115
+ return lines.slice(startIdx, endIdxExclusive).join('\n')
116
+ }
117
+
118
+ export type PremiseCheckResult = { pass: boolean; failures: string[] }
119
+
120
+ /**
121
+ * Re-assert every parsed premise against the current file content, injected
122
+ * via `fileReader` (returns `null` when the path does not exist). One
123
+ * failure line per broken pin, naming the exact path/kind/value so the
124
+ * Developer can see precisely what moved.
125
+ */
126
+ export function checkPremises(
127
+ assertions: PremiseAssertion[],
128
+ fileReader: (path: string) => string | null
129
+ ): PremiseCheckResult {
130
+ const failures: string[] = []
131
+
132
+ for (const a of assertions) {
133
+ const content = fileReader(a.path)
134
+ if (content === null) {
135
+ failures.push(
136
+ `premise-check: ${a.path} does not exist on disk (premise asserted \`${a.kind}: ${a.value}\`) — the surface moved since this brief was authored; re-dig before proceeding.`
137
+ )
138
+ continue
139
+ }
140
+
141
+ if (a.kind === 'contains' && !content.includes(a.value)) {
142
+ failures.push(
143
+ `premise-check: ${a.path} no longer contains "${a.value}" — the premise this brief pinned has moved; re-dig before proceeding.`
144
+ )
145
+ } else if (a.kind === 'absent' && content.includes(a.value)) {
146
+ failures.push(
147
+ `premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`
148
+ )
149
+ } else if (a.kind === 'sha256') {
150
+ const actual = createHash('sha256').update(content).digest('hex')
151
+ if (actual !== a.value.toLowerCase()) {
152
+ failures.push(
153
+ `premise-check: ${a.path} sha256 mismatch (brief pinned ${a.value}, file is now ${actual}) — the file changed since this brief was authored; re-dig before proceeding.`
154
+ )
155
+ }
156
+ }
157
+ }
158
+
159
+ return { pass: failures.length === 0, failures }
160
+ }