@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,163 @@
1
+ /**
2
+ * Retired vocabulary — mechanisms genuinely gone from AEG's own methodology
3
+ * (the decision log, the lock, the `team-leader` role, the `D-###` decision
4
+ * id, …) with no legitimate positive occurrence anywhere but the exempt
5
+ * lists below. This is `retired-vocabulary.test.ts`'s `RETIRED` array and
6
+ * the non-citation members of its `RETIRED_IN_PRODUCT` array (both target
7
+ * the same retired-mechanism concern; the split between the two vitest
8
+ * arrays there is organic history, not a semantic boundary) — the file this
9
+ * module was extracted from names the incident: five review rounds each
10
+ * declared the decision-log claim swept, and each time it resurfaced a few
11
+ * lines from where the previous pass fixed it. A rule enforced by attention,
12
+ * not a gate.
13
+ *
14
+ * Deliberately NOT `RETIRED_IN_PRODUCT`'s forge-number / tranche-slug /
15
+ * legacy-slug patterns: those ban a LIVE, unexplained citation, not a
16
+ * retired concept, and are already this package's own
17
+ * `checkUnresolvableReferences` (`reader-resolvable-prose.ts`) — shipping
18
+ * both under two check names would double-report the identical match. This
19
+ * module is deliberately the narrower half of that file's original scan.
20
+ *
21
+ * Content copied verbatim from `retired-vocabulary.test.ts`'s `RETIRED`
22
+ * (minus its citation members), `EXEMPT`, and the non-citation entries of
23
+ * `PATTERN_EXEMPT`, never re-derived — this module's whole purpose is
24
+ * giving that list a CheckSpec adapter, not re-litigating it. Every pattern
25
+ * here is POSIX-ERE-safe (no `\d`/`\w`/`\s`, no lookaheads) by the same
26
+ * discipline `reader-resolvable-prose.ts` documents, so `grep -E` (the
27
+ * vitest suite) and native `RegExp` (this evaluator) agree on every match.
28
+ *
29
+ * Zero I/O: every input (file paths + contents) is read by the adapter and
30
+ * passed in, same charter as `reader-resolvable-prose.ts`.
31
+ */
32
+
33
+ export type VocabSourceFile = { path: string; content: string }
34
+
35
+ export type VocabFinding = {
36
+ file: string
37
+ line: number
38
+ pattern: string
39
+ message: string
40
+ }
41
+
42
+ /** The retired mechanisms this scan bans as claims of current-ness — see the module header for what is deliberately NOT here. */
43
+ export const RETIRED_PATTERNS: readonly string[] = [
44
+ // the deleted decision-id format
45
+ String.raw`\bD-[0-9]{3}\b`,
46
+ 'D-(###|NNN|nnn|xxx)',
47
+ // the retired record as a live, writable, required artifact, every
48
+ // inflection but never "decision logic"
49
+ 'decision (entry|logged)',
50
+ 'decision-log entry',
51
+ 'decision log entry',
52
+ 'decision log(s|ged|ging)?([^a-z]|$)',
53
+ 'decision-log',
54
+ 'decision entr',
55
+ 'decisions? (are |is )?logged',
56
+ 'log entries',
57
+ String.raw`decisions\.md`,
58
+ 'decisions-legacy',
59
+ 'CONTRADICTION',
60
+ 'assumes Tier 3',
61
+ // the lock
62
+ 'Lock: ?YES',
63
+ 'Lock: ?NO',
64
+ 'lock approvals?',
65
+ 'approves locks',
66
+ 'Conforms to lock',
67
+ 'Challenges lock',
68
+ // checks deleted with the machinery
69
+ 'checkDecisionNumbersFresh',
70
+ // the role that no longer exists
71
+ String.raw`roles/team-leader\.md`,
72
+ // the wreckage a citation strip leaves when it deletes the contents of a
73
+ // parenthetical and not the punctuation around it — the shape is banned,
74
+ // not one instance of it. `()` is deliberately absent: it is an ordinary
75
+ // function call, and banning it would make any TypeScript source unusable.
76
+ String.raw`\(,`,
77
+ String.raw`,\)`,
78
+ String.raw`\(\.\)`
79
+ ]
80
+
81
+ /** Paths where a retired-vocabulary mention is legitimate: frozen archives and historical records. Substring match, same as the vitest suite's own `EXEMPT`. */
82
+ export const RETIRED_EXEMPT_SUBSTRINGS: readonly string[] = [
83
+ 'docs/decisions-legacy.md',
84
+ 'apps/herald-ai/docs/',
85
+ 'apps/vada-ai/docs/',
86
+ 'aeg-root/tranches/completed/',
87
+ 'packages/aeg-core/src/docs/published-prose',
88
+ 'packages/aeg-core/src/retired-vocabulary.test.ts',
89
+ 'packages/aeg-core/src/retired-vocabulary.ts',
90
+ '/fixtures/',
91
+ '/node_modules/',
92
+ '/.next/',
93
+ '/.turbo/'
94
+ ]
95
+
96
+ /**
97
+ * Per-pattern exemptions on top of `RETIRED_EXEMPT_SUBSTRINGS`, for the
98
+ * handful of patterns with a real, load-bearing false-positive shape (a
99
+ * live doc that legitimately discusses the retired concept by name, or a
100
+ * same-shaped live feature). Copied verbatim from `retired-vocabulary.test.ts`'s
101
+ * `PATTERN_EXEMPT` — including entries for `apps/herald-ai`/`apps/vada-ai`
102
+ * paths this repo does not carry, since a nonexistent path costs nothing to
103
+ * keep and this module's whole point is not re-deriving the list.
104
+ */
105
+ export const PATTERN_EXEMPT: Readonly<Record<string, readonly string[]>> = {
106
+ [String.raw`\bD-[0-9]{3}\b`]: ['./CLAUDE.md'],
107
+ [String.raw`decisions\.md`]: [
108
+ 'docs-index.md',
109
+ 'apps/vada-ai/specs/legacy/README.md',
110
+ 'apps/vada-ai/specs/vada-product-spec.md',
111
+ 'apps/vada-ai/web/CLAUDE.md',
112
+ 'apps/vada-ai/CLAUDE.md'
113
+ ],
114
+ 'decisions-legacy': [
115
+ 'packages/aeg-core/src/file-classify.ts',
116
+ 'packages/aeg-core/src/file-classify.test.ts',
117
+ 'packages/aeg-core/src/pr-tier.test.ts',
118
+ 'docs-index.md',
119
+ '.claude/skills/database/SKILL.md',
120
+ '.claude/skills/vada-architecture/SKILL.md',
121
+ 'apps/vada-ai/specs/vada-byok-principles.md',
122
+ 'apps/vada-ai/web/CLAUDE.md',
123
+ 'apps/vada-ai/CLAUDE.md'
124
+ ],
125
+ CONTRADICTION: ['apps/vada-ai/specs/vada-reviewers-spec.md']
126
+ }
127
+
128
+ function lineNumbers(content: string, pattern: RegExp): number[] {
129
+ const out: number[] = []
130
+ const lines = content.split('\n')
131
+ for (let i = 0; i < lines.length; i++) {
132
+ if (pattern.test(lines[i] ?? '')) out.push(i + 1)
133
+ }
134
+ return out
135
+ }
136
+
137
+ /**
138
+ * Sweeps `files` for `RETIRED_PATTERNS`, skipping any file whose path
139
+ * contains a `RETIRED_EXEMPT_SUBSTRINGS` entry, or that pattern's own
140
+ * `PATTERN_EXEMPT` entry. Matching is per-line, case-sensitive — the same
141
+ * semantics `grep -E` (with no `-i`) gives the vitest suite this was
142
+ * extracted from.
143
+ */
144
+ export function scanRetiredVocabulary(files: readonly VocabSourceFile[]): VocabFinding[] {
145
+ const findings: VocabFinding[] = []
146
+ for (const file of files) {
147
+ if (RETIRED_EXEMPT_SUBSTRINGS.some((e) => file.path.includes(e))) continue
148
+ for (const pattern of RETIRED_PATTERNS) {
149
+ const patternExempt = PATTERN_EXEMPT[pattern] ?? []
150
+ if (patternExempt.some((e) => file.path.includes(e))) continue
151
+ const re = new RegExp(pattern)
152
+ for (const line of lineNumbers(file.content, re)) {
153
+ findings.push({
154
+ file: file.path,
155
+ line,
156
+ pattern,
157
+ message: `claims a retired AEG mechanism is still live (matches /${pattern}/)`
158
+ })
159
+ }
160
+ }
161
+ }
162
+ return findings
163
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Required pre-merge review gate (aeg-review-gate-v1 task 1, #474). Blocks a
3
+ * PR from merging unless a clean code-reviewer `APPROVE` verdict
4
+ * AND a clean security-review `PASS` verdict both exist on the PR — the same
5
+ * `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` detection
6
+ * (`verdict-extraction.ts`) the post-merge Archivist automation already runs,
7
+ * now gated pre-merge and blocking instead of post-merge and advisory-only.
8
+ *
9
+ * Verdict comments are ONLY counted when their author is on the same
10
+ * `PRINCIPAL_ALLOWLIST` the waiver actor-check trusts (security finding,
11
+ * PR #806): body-shape alone is never sufficient on a public repo. Unverified
12
+ * comments are ignored, not fatal.
13
+ *
14
+ * A verified `vinaya/waiver:review` label (the exact actor-verification pattern,
15
+ * `isWaiverLabelActorVerified` reused directly and parameterized by label —
16
+ * see `waiver-label.ts`) lets a principal explicitly skip the requirement for
17
+ * one PR. Label presence alone is never sufficient — only an actor-verified
18
+ * label waives the gate, mirroring exactly.
19
+ *
20
+ * Reviewed-commit binding (#73, a duplicate of #71 closes this one). A clean
21
+ * verdict is no longer sufficient on its own — it must also cover the PR's
22
+ * CURRENT head. Design record (the four questions this fix had to answer,
23
+ * argued in full in the closing PR's body):
24
+ *
25
+ * 1. Commit sha, not tree hash. `headSha` is `gh pr view --json headRefOid`
26
+ * verbatim — one API call, no second fetch of a commit object's tree.
27
+ * Matches the Issue's own proposed fix shape and the hand-typed
28
+ * convention reviewers were already using ("Judged head: <sha>", never a
29
+ * tree hash). Cost accepted: a rebase or an empty amend that leaves the
30
+ * tree byte-identical still invalidates every verdict — no free pass for
31
+ * "the code didn't really change." That is the explicit trade for a
32
+ * single mental model ("a verdict covers an exact commit") over a cheaper
33
+ * but subtler one (two different code states could share a tree).
34
+ * 2. Every push that changes `headRefOid` invalidates unconditionally — a
35
+ * rebase with an identical tree, a merge commit resolving a conflict
36
+ * elsewhere, and a genuine content-changing amend all produce a new sha
37
+ * and therefore a new required verdict. Only a push that does NOT change
38
+ * `headRefOid` at all (a body-only PR-description edit; a force-push that
39
+ * reproduces the exact same commit object) leaves an existing binding
40
+ * intact, because there is nothing for it to have gone stale against.
41
+ * 3. `headSha` is REQUIRED on `ReviewGateInput`, not optional. An optional
42
+ * field that silently skips the binding check when absent is fail-open —
43
+ * exactly the defect class this task exists to close. Every caller
44
+ * (`bin/verify-review-gate.ts`, `apps/cli/src/checks/bin/check-review-gate.ts`)
45
+ * must supply it; the type system enforces that, not a runtime default.
46
+ * 4. Fail-closed for the transition. The moment this merges, every verdict
47
+ * already posted on an open PR is unbound (it carries no `Judged head:`
48
+ * line) and the gate stops honouring it — expensive, and correct: an
49
+ * unbound verdict is exactly the property this task closes. The sanctioned
50
+ * escape for a PR already far along in review is the existing
51
+ * `vinaya/waiver:review` actor-verified label, applied by a principal, the
52
+ * same mechanism that already exists for any other one-off skip.
53
+ *
54
+ * Pure — no `fs`, no `fetch`, no `process.env`. The CLI shim
55
+ * (`bin/verify-review-gate.ts`) resolves the PR's comments/labels/label-actor/
56
+ * head sha via `gh` and calls `checkReviewGate`.
57
+ */
58
+
59
+ import { isPrincipal, isWaiverLabelActorVerified, PRINCIPAL_ALLOWLIST, WAIVER_LABEL_REVIEW } from './waiver-label'
60
+ import { extractCodeReviewVerdict, extractSecurityReviewVerdict } from './verdict-extraction'
61
+
62
+ export type ReviewGateVerdict = 'pass' | 'fail'
63
+
64
+ export type ReviewGateResult = {
65
+ verdict: ReviewGateVerdict
66
+ reason: string
67
+ waived: boolean
68
+ }
69
+
70
+ export type ReviewGateComment = {
71
+ body: string
72
+ /** The comment author's GitHub login, or `null` when the caller could not resolve one. */
73
+ author: string | null
74
+ }
75
+
76
+ export type ReviewGateInput = {
77
+ /** Every comment on the PR, with its author. */
78
+ comments: ReviewGateComment[]
79
+ /** Every label currently applied to the PR. */
80
+ labels: string[]
81
+ /** Actor of the most recent `vinaya/waiver:review` labeling timeline event, or `null` when none exists. */
82
+ waiverLabelActor: string | null
83
+ /**
84
+ * The PR's current head commit sha (`gh pr view --json headRefOid`),
85
+ * resolved from GitHub — never from local git, an env var, or the PR's own
86
+ * checkout, all three of which a `pull_request`-triggered workflow's
87
+ * PR-editable YAML could steer (#73). REQUIRED, not optional: an omitted
88
+ * head would have to mean either skip-the-binding-check (fail-open, the
89
+ * exact defect this field exists to close) or unconditional-fail, and a
90
+ * required field makes that choice a compile error instead of a runtime
91
+ * default. Every verdict must cover this value to count as clean.
92
+ */
93
+ headSha: string
94
+ /**
95
+ * Overrides `PRINCIPAL_ALLOWLIST` for this evaluation when provided — an
96
+ * adopter repo's own `vinaya.config.json` `principals` field, resolved by
97
+ * the CLI bin before calling in (never read from here; this stays pure).
98
+ * Defaults to `PRINCIPAL_ALLOWLIST` when omitted, so every existing caller
99
+ * (this repo's own `bin/verify-review-gate.ts` included) is unaffected.
100
+ * `PRINCIPAL_ALLOWLIST` hardcoding this repo's own principal made the gate
101
+ * structurally unpassable on any adopter repo — found live on a real
102
+ * client repo's first dispatched task, the reviewer/security verdicts it
103
+ * already had counted for nobody.
104
+ */
105
+ principalAllowlist?: string[]
106
+ }
107
+
108
+ /**
109
+ * Branch names are contributor-controlled metadata, so none can exempt an
110
+ * authority check. The former `plan/*` exemption assumed those branches
111
+ * contained topology docs only; a contributor could put code on a branch
112
+ * with that prefix and make the adapter exit before it fetched the PR. Keep
113
+ * this exported predicate for API compatibility, but fail closed for every
114
+ * branch. A future plan-only exemption would need server-derived changed-file
115
+ * validation, not a name prefix.
116
+ */
117
+ export function isReviewGateExemptBranch(_branch: string): boolean {
118
+ return false
119
+ }
120
+
121
+ /**
122
+ * True when `extraction.headSha` covers `headSha` — an exact match, or
123
+ * `headSha` starting with `extraction.headSha` (the abbreviated-sha case:
124
+ * a verdict may bind against a 7-char prefix, and `headSha` itself is always
125
+ * the full 40-char form GitHub's API returns). `false` when the extraction
126
+ * carries no `headSha` at all (no `Judged head:` line was found) — an
127
+ * unbound verdict never counts as covering anything.
128
+ */
129
+ function isBoundToHead(extraction: { headSha: string | null }, headSha: string): boolean {
130
+ if (!extraction.headSha) return false
131
+ return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase())
132
+ }
133
+
134
+ /**
135
+ * `pass` when either (a) `vinaya/waiver:review` is present and actor-verified against
136
+ * `PRINCIPAL_ALLOWLIST`, or (b) both verdicts are clean AND bound — code-reviewer
137
+ * `APPROVE` (not `REQUEST_CHANGES`, not missing, not unclear) covering the PR's
138
+ * current `headSha`, and security-review `PASS` (not `FAIL`, not missing, not
139
+ * unclear) covering it too. `fail` otherwise, naming exactly which verdict(s)
140
+ * are not clean, not bound to the current head, or both.
141
+ */
142
+ export function checkReviewGate(input: ReviewGateInput): ReviewGateResult {
143
+ const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST
144
+ const waived = isWaiverLabelActorVerified({
145
+ label: WAIVER_LABEL_REVIEW,
146
+ labels: input.labels,
147
+ labelActor: input.waiverLabelActor,
148
+ principalAllowlist
149
+ })
150
+ if (waived) {
151
+ return {
152
+ verdict: 'pass',
153
+ reason: `\`${WAIVER_LABEL_REVIEW}\` label is actor-verified — review requirement waived for this PR.`,
154
+ waived: true
155
+ }
156
+ }
157
+
158
+ // Verdict-AUTHOR verification (security finding on PR #806): on a public
159
+ // repo any GitHub account can post a `VERDICT: APPROVE`-shaped comment, and
160
+ // most-recent-clear-hit-wins extraction would let a forged later APPROVE
161
+ // override a real earlier REQUEST CHANGES. Only comments whose author is on
162
+ // the same `PRINCIPAL_ALLOWLIST` the waiver's actor check already trusts
163
+ // participate in verdict extraction; everything else — unknown authors and
164
+ // unresolvable (`null`) ones alike — is IGNORED, never fatal, so a drive-by
165
+ // comment cannot brick evaluation, only fail to count. Dispatched reviewer
166
+ // agents post under the principal's own `gh` identity, so the legitimate
167
+ // flow is unchanged.
168
+ const verified = input.comments.filter((c) => isPrincipal(c.author, principalAllowlist))
169
+ // Count only VERDICT-shaped ignored comments — deployment bots and ordinary
170
+ // chat are also non-allowlisted, and counting them would imply forgery
171
+ // where there is only noise (review finding, PR #806).
172
+ const ignoredCount = input.comments.filter(
173
+ (c) => !isPrincipal(c.author, principalAllowlist) && c.body.includes('VERDICT')
174
+ ).length
175
+ const verifiedBodies = verified.map((c) => c.body)
176
+
177
+ const codeReview = extractCodeReviewVerdict(verifiedBodies)
178
+ const security = extractSecurityReviewVerdict(verifiedBodies)
179
+ const codeReviewClean = codeReview.value === 'APPROVE'
180
+ const securityClean = security.value === 'PASS'
181
+ const codeReviewBound = isBoundToHead(codeReview, input.headSha)
182
+ const securityBound = isBoundToHead(security, input.headSha)
183
+
184
+ if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
185
+ return {
186
+ verdict: 'pass',
187
+ reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
188
+ waived: false
189
+ }
190
+ }
191
+
192
+ const problems: string[] = []
193
+ if (!codeReviewClean) {
194
+ problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`)
195
+ } else if (!codeReviewBound) {
196
+ problems.push(
197
+ `the newest code-review verdict covers ${codeReview.headSha ?? 'no recorded commit'}, head is ${input.headSha}`
198
+ )
199
+ }
200
+ if (!securityClean) {
201
+ problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`)
202
+ } else if (!securityBound) {
203
+ problems.push(
204
+ `the newest security-review verdict covers ${security.headSha ?? 'no recorded commit'}, head is ${input.headSha}`
205
+ )
206
+ }
207
+ const ignoredNote =
208
+ ignoredCount > 0
209
+ ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.`
210
+ : ''
211
+
212
+ return {
213
+ verdict: 'fail',
214
+ reason: `${problems.join('; ')}. A principal can apply an actor-verified \`${WAIVER_LABEL_REVIEW}\` label to skip this requirement, or post the missing/clean verdict comment(s).${ignoredNote}`,
215
+ waived: false
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Exported so a caller can cheaply pre-check the branch BEFORE paying for
221
+ * whatever it takes to resolve `expectedAuthor` — resolving that value is a
222
+ * network round-trip (`resolveReleaseActor(loadTrustAnchorConfig())`) that
223
+ * must not run on every ordinary PR just because it is one of three
224
+ * arguments to `isChangesetsReleasePr`. Found live (code review, PR #169):
225
+ * evaluating it inline as a function argument runs it unconditionally,
226
+ * regardless of branch, since JS evaluates arguments eagerly.
227
+ */
228
+ export const CHANGESET_RELEASE_BRANCH = 'changeset-release/main'
229
+
230
+ /**
231
+ * The stock Changesets flow's default identity — a release PR opened by
232
+ * `changesets/action` using the ambient `GITHUB_TOKEN` shows this as its
233
+ * author. Only a SANE DEFAULT for adopters who haven't configured
234
+ * `releaseActor` — never assume it matches any specific repo's real setup.
235
+ * A repo that opens release PRs with a custom PAT (this repo's own
236
+ * `RELEASE_TOKEN`, `.github/workflows/release.yml`) has a DIFFERENT real PR
237
+ * author (the token's owner) and must set `releaseActor` in its
238
+ * `vinaya.config.json`.
239
+ */
240
+ export const DEFAULT_RELEASE_ACTOR = 'github-actions[bot]'
241
+
242
+ /**
243
+ * True only for the Changesets release PR. `branch` and `author` must both
244
+ * match — `branch` alone is not a trust boundary, an attacker can push a
245
+ * branch literally called `changeset-release/main`. `expectedAuthor` is
246
+ * caller-resolved, never hardcoded here — this ships in the published
247
+ * `@attalabs/vinaya` package, and hardcoding any one identity would be
248
+ * correct for at most one adopter's release-token setup. Callers resolve it
249
+ * via `resolveReleaseActor(loadTrustAnchorConfig())`, falling back to
250
+ * `DEFAULT_RELEASE_ACTOR`.
251
+ *
252
+ * This predicate is pure and trust-agnostic about ITS OWN inputs — the
253
+ * caller is entirely responsible for where `branch`/`author` come from.
254
+ * `check-body-bare-digits.ts` is the one caller, and only reachable in
255
+ * production from `vinaya-body-checks.yml`'s `pull_request_target` job:
256
+ * `branch`/`author` there come from a live `gh pr view <PR_NUMBER>` fetch,
257
+ * with `PR_NUMBER` itself sourced from `github.event.pull_request.number` —
258
+ * an expression evaluated from the workflow file on the DEFAULT BRANCH, a
259
+ * pull request cannot edit that file to substitute a different literal
260
+ * (the same `pull_request_target` boundary `vinaya-review.yml` already
261
+ * uses). Found live (round 5, PR #165): the identical exemption on a plain
262
+ * `pull_request` trigger let an attacker redirect `PR_NUMBER`/`BRANCH` to
263
+ * any already-approved PR by the configured release actor — verified no
264
+ * env-var or git-state signal inside that trigger type is a safe anchor,
265
+ * which is why this predicate never runs there again.
266
+ */
267
+ export function isChangesetsReleasePr(branch: string, author: string | null, expectedAuthor: string): boolean {
268
+ return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor
269
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Single-plan-PR guard predicate (task 19 / #336). Extracted from
3
+ * `bin/open-pr.ts` (aeg-governance-hardening task 24, #364, Part 1) so the
4
+ * exact same implementation can be consumed by both the ring-0 local wrapper
5
+ * (`open-pr.ts`, prevention) and a ring-1 CI check (detection, for PRs opened
6
+ * via the web UI that bypass the wrapper entirely) — one implementation per
7
+ * fact (§11 constraint), never a second copy of this predicate.
8
+ *
9
+ * Pure — no `fs`, no `gh`/`git` I/O. Callers gather the facts (this branch's
10
+ * touched files, every other open PR's touched files) and pass them in.
11
+ */
12
+
13
+ /**
14
+ * Parses a tranche slug from a touched file path, when that path is an
15
+ * active (non-`completed/`) tranche topology file. Returns `null` for
16
+ * everything else — including `README.md` and `*.tokens.md`, neither of
17
+ * which is a topology file the single-plan-PR guard (below) cares about.
18
+ */
19
+ export function trancheSlugFromTopologyPath(path: string): string | null {
20
+ const m = path.match(/^aeg-root\/tranches\/([^/]+)\.md$/)
21
+ if (!m) return null
22
+ const slug = m[1] as string
23
+ if (slug === 'README' || slug.endsWith('.tokens')) return null
24
+ return slug
25
+ }
26
+
27
+ export type OpenPrFiles = { number: number; files: string[] }
28
+
29
+ /**
30
+ * Single-plan-PR guard (task 19 / #336): refuses a plan-branch diff
31
+ * that touches a tranche's topology file when another OPEN PR's diff
32
+ * already touches that SAME tranche's topology file. Ends the plan-PR
33
+ * race that produced two concurrent plan PRs for `aeg-governance-hardening`
34
+ * itself (#352/#354) — each cut from `origin/main` unaware of the other's
35
+ * newly-cut Issue.
36
+ *
37
+ * `branchFiles` is this branch's diff vs `origin/main` (or vs the PR's
38
+ * base); `otherOpenPrs` is every other currently-open PR's touched files
39
+ * (the caller excludes this PR's own number when editing). An ordinary
40
+ * Dormant where plans are forge objects: with a tranche held as a
41
+ * Milestone plus labeled Issues, no plan diff touches a topology file and
42
+ * this predicate has nothing to compare. It stays because it is still the
43
+ * right guard for a repo that keeps plans as files — a dormant check that
44
+ * says so is honest; one that reads as live protection is not.
45
+ *
46
+ * task-branch PR touches no topology file at all, so `branchFiles` yields
47
+ * no slugs and this passes trivially without even needing `otherOpenPrs`.
48
+ */
49
+ export function checkSinglePlanPr(
50
+ branchFiles: string[],
51
+ otherOpenPrs: OpenPrFiles[]
52
+ ): { ok: boolean; message?: string } {
53
+ const touchedSlugs = new Set(branchFiles.map(trancheSlugFromTopologyPath).filter((s): s is string => s !== null))
54
+ if (touchedSlugs.size === 0) return { ok: true }
55
+
56
+ for (const pr of otherOpenPrs) {
57
+ const otherSlugs = new Set(pr.files.map(trancheSlugFromTopologyPath).filter((s): s is string => s !== null))
58
+ for (const slug of touchedSlugs) {
59
+ if (otherSlugs.has(slug)) {
60
+ return {
61
+ ok: false,
62
+ message: `single-plan-pr: another open PR (#${pr.number}) already touches tranche "${slug}"'s topology file. Only one open plan PR per tranche is allowed at a time — wait for #${pr.number} to merge or close, or coordinate with its author.`
63
+ }
64
+ }
65
+ }
66
+ }
67
+ return { ok: true }
68
+ }
69
+
70
+ /** True when any of `files` touches an active tranche's topology file — i.e. this diff is a plan-PR diff. */
71
+ export function touchesAnyTopology(files: string[]): boolean {
72
+ return files.some((f) => trancheSlugFromTopologyPath(f) !== null)
73
+ }