@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,174 @@
1
+ /**
2
+ * Typed model for AEG artifacts. See `aeg-root/tranche-model.md` for the
3
+ * canonical specification (§3 status table, §4 thin-file template).
4
+ *
5
+ * This module is pure: no I/O. The parser produces these shapes from file
6
+ * contents; `deriveTranche` consumes them plus a `ForgeFacts` snapshot.
7
+ */
8
+
9
+ import type { ForgeFacts, Tranche, Lifecycle, Task } from '@attalabs/aeg-types'
10
+
11
+ // ---------- Registry (projects.md) ----------
12
+
13
+ export type Project = {
14
+ /** Registry-unique name. Authority for `Project:` validation. */
15
+ name: string
16
+ /** Declared home folder (e.g. `apps/<product>`). Never derived. */
17
+ path: string
18
+ /** Specs folder for the project. */
19
+ specsPath: string
20
+ /**
21
+ * Per-project state folder, or `null` when the cell holds prose like
22
+ * "state tracked globally for now" — i.e. state isn't tracked under this
23
+ * project's path.
24
+ */
25
+ statePath: string | null
26
+ }
27
+
28
+ export type Registry = Project[]
29
+
30
+ // ---------- Tranche file (tranches/<name>.md) ----------
31
+
32
+ /**
33
+ * `Lifecycle`/`Task`/`Tranche` live in `@attalabs/aeg-types` (task
34
+ * aeg-forge-state-v1 3a) — re-exported here since every existing call site
35
+ * across the repo imports them from `@attalabs/aeg-core`. Moved out so
36
+ * `@attalabs/aeg-forge-state` can depend on these shapes without creating a
37
+ * package cycle with `aeg-core` (which in turn depends on
38
+ * `aeg-forge-state`'s derivation function).
39
+ */
40
+ export type { Lifecycle, Task, Tranche }
41
+
42
+ // ---------- Forge facts ----------
43
+
44
+ /**
45
+ * `ForgeFacts` lives in `@attalabs/aeg-types` (aeg-core-purity fix, #521) —
46
+ * re-exported here for the same reason as `Lifecycle`/`Task`/`Tranche`:
47
+ * every existing call site imports it from `@attalabs/aeg-core`, and it moved
48
+ * out so the I/O-performing fetchers that produce it (now in
49
+ * `@attalabs/aeg-forge-state`) don't need to depend on `aeg-core`.
50
+ */
51
+ export type { ForgeFacts }
52
+
53
+ // ---------- Derivation output ----------
54
+
55
+ /**
56
+ * The statuses derivation can conclude. The *rules* that produce them are not
57
+ * written here or in `derive-tranche.ts` — they live as an ordered,
58
+ * pure-data list in `state-machine-model.ts` (the `actions.ts`
59
+ * discipline), which `deriveStatus` executes and the docs render from the
60
+ * same list.
61
+ *
62
+ * `backlog` remains a member because it is a project-level concept consumers
63
+ * still render; derivation never emits it inside a tranche.
64
+ *
65
+ * `dropped` and `incoherent` are the two **honest terminal** statuses.
66
+ * A closed Issue with no merged PR must never resolve to `todo` (which implies
67
+ * not-started). `dropped` = closed `NOT_PLANNED` (legitimately abandoned);
68
+ * `incoherent` = closed `COMPLETED` but with no merged-PR link (genuinely done
69
+ * yet unprovable, or a broken close). Both are surfaced, never hidden.
70
+ */
71
+ export type DerivedStatus =
72
+ | 'backlog'
73
+ | 'todo'
74
+ | 'in-flight'
75
+ | 'in-review'
76
+ | 'changes-requested'
77
+ | 'merged'
78
+ | 'blocked'
79
+ | 'dropped'
80
+ | 'incoherent'
81
+
82
+ export type DispatchBlockers = {
83
+ /** Depends-on edges whose target is not yet `merged`. */
84
+ dependsOnNotMerged: string[]
85
+ /**
86
+ * Conflicts-with edges whose target is currently occupying its collision
87
+ * domain — derived status `in-flight`, `in-review`, or `changes-requested`
88
+ * (§8 conflict gate).
89
+ */
90
+ conflictsWithOpenOrInFlight: string[]
91
+ }
92
+
93
+ export type DerivedTask = {
94
+ task: Task
95
+ status: DerivedStatus
96
+ /**
97
+ * `true` when the §8 gates allow dispatching this task: all `dependsOn`
98
+ * targets are `merged`, and no `conflictsWith` target is open/in-flight.
99
+ * Dispatchability is independent of this task's own current status — a
100
+ * task already `in-review` is still "dispatchable" by this definition (the
101
+ * gates would have allowed it to start).
102
+ */
103
+ dispatchable: boolean
104
+ blockers: DispatchBlockers
105
+ }
106
+
107
+ export type UnknownEdge = {
108
+ from: string
109
+ to: string
110
+ kind: 'depends-on' | 'conflicts-with'
111
+ }
112
+
113
+ export type DerivedTranche = {
114
+ tranche: Tranche
115
+ tasks: DerivedTask[]
116
+ /**
117
+ * Edges that reference task ids not present in this tranche's topology
118
+ * table. Surface for diagnostics; does not throw, since real files
119
+ * occasionally name dropped/prose-only ids (e.g. a `3a` that survives only
120
+ * in a tranche file's narrative).
121
+ */
122
+ unknownEdges: UnknownEdge[]
123
+ }
124
+
125
+ // ---------- Token ledger (tranches/<name>.tokens.md) ----------
126
+
127
+ /**
128
+ * One row of the append-only per-tranche token/cost ledger. See
129
+ * `aeg-root/tranche-model.md` §12 for the canonical format. Each role
130
+ * appends one row at the end of its turn; re-entry appends another row.
131
+ * The tranche total is `sum(rows)`, derived at read time — never stored.
132
+ *
133
+ * The two capture sources surface here as `null` cells: a row written by a
134
+ * claude.ai role (Planner / Brief Author / Reviewer / Security) before the
135
+ * Principal has filled in its usage figure has `tokensIn`/`tokensOut`/`cost`
136
+ * as `null`; a terminal-role row (Developer / Archivist with `/cost`) has
137
+ * exact numbers.
138
+ */
139
+ export type LedgerRow = {
140
+ /** Free-text phase identifier. Convention: `<task-id>: <phase>` per task
141
+ * (e.g. `9: develop`), or a bare phase for tranche-wide work (e.g.
142
+ * `planning`). The parser does not enforce the convention — the Phase
143
+ * cell is opaque so the table can be re-pivoted later. */
144
+ phase: string
145
+ role: string
146
+ /** Free-text — the role's agent + model (e.g. `claude-opus-4-7 (CC)`,
147
+ * `claude-opus-4-7 (chat)`). The PRICING table dependency is on the
148
+ * model name, not on the parser. */
149
+ agentModel: string
150
+ /** `null` when the cell was `—` / `-` / empty (unknown — claude.ai
151
+ * row not yet filled). */
152
+ tokensIn: number | null
153
+ tokensOut: number | null
154
+ /** Cost in USD as a number. `null` for unknown. Cost depends on the
155
+ * adapter's PRICING table — when the model is missing from it, this is
156
+ * `0` from the source row, not `null` (a known limitation, not a parser
157
+ * bug). */
158
+ cost: number | null
159
+ /** Raw date cell — typically `YYYY-MM-DD`. Captured as a string so the
160
+ * model docs can evolve the format without breaking the parser. */
161
+ date: string
162
+ }
163
+
164
+ /**
165
+ * `sumLedger(rows)` output — pure aggregate over the rows in source order.
166
+ * Null cells contribute zero; the row count is preserved so a UI can show
167
+ * "N entries totalling …".
168
+ */
169
+ export type LedgerTotals = {
170
+ tokensIn: number
171
+ tokensOut: number
172
+ cost: number
173
+ rows: number
174
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Shared code-reviewer / security-review verdict extraction (aeg-review-gate-v1
3
+ * task 1, #474). Originally private to `archive-task.ts`'s post-merge
4
+ * provenance assembly (advisory-only, DANGLING on a missing/unclear verdict).
5
+ * Extracted so the pre-merge `review-gate.ts` blocking check calls the
6
+ * IDENTICAL regex/parsing logic — a future drift between two copies of the
7
+ * same pattern would silently reopen the gap this task exists to close (one
8
+ * implementation per fact, §11 constraint). Pure — no `fs`, no `fetch`.
9
+ *
10
+ * Picks the MOST RECENT comment carrying a clear value, not the first comment
11
+ * merely matching the pattern (aeg-review-gate-v1 task 1 fix — the original
12
+ * single-comment `.find()` broke on real multi-comment PRs: a REQUEST_CHANGES
13
+ * verdict followed by fixes and a later clean APPROVE).
14
+ *
15
+ * Line-anchored `VERDICT:` marker only — NOT a bare word search anywhere in
16
+ * the comment (aeg-review-gate-v1 task 1 follow-up, security-review FAIL
17
+ * finding, confirmed by direct execution: the old bare `\b(PASS|FAIL)\b`
18
+ * pattern matched the post-merge Archivist's own auto-generated DANGLING
19
+ * placeholder text — "no security-review pass was run before merge —
20
+ * DANGLING, see below" — as a clean PASS, since it contains the standalone
21
+ * word "pass". Same risk in reverse for "APPROVE" inside ordinary prose,
22
+ * e.g. "I do NOT approve of that design"). `^\s*VERDICT:\s*<value>\b`,
23
+ * multiline, is the real subagent contract (`roles/reviewer.md`/
24
+ * `roles/security.md`'s own VERDICT block, which `.claude/agents/
25
+ * code-reviewer.md`/`security-reviewer.md` require verbatim) — tightening
26
+ * to it closes the gap without inventing a new convention.
27
+ *
28
+ * The VALUE is windowed to the comment's first THREE lines only
29
+ * (review-convergence-v1 task 2, round 4, `#392`) — never a line anywhere
30
+ * else in the body. Every shape this package's own callers render puts the
31
+ * `VERDICT:`/`ESCALATE:` line first and `Judged head:` third; a
32
+ * caller-supplied field (findings, conformance prose, a summary) never
33
+ * renders before line 5. Restricting the read window closes a
34
+ * caller-controlled-text injection route without narrowing what any real
35
+ * render needs matched.
36
+ *
37
+ * The CANDIDATE SET stays whole-body (review-convergence-v1 task 2, round 5,
38
+ * `#392`) — round 4 windowed where a verdict's value is read, not which
39
+ * comments count as a candidate at all. A comment whose only VERDICT-shaped
40
+ * line sits outside its first three lines is still the most recent
41
+ * candidate if it is the most recent comment; it reads as DANGLING rather
42
+ * than being skipped, so it correctly shadows an earlier clean verdict
43
+ * instead of silently letting the earlier one win.
44
+ *
45
+ * The anchor tolerates a leading markdown EMPHASIS run — one to three `*` or
46
+ * `_`, immediately abutting the token — so `**VERDICT: APPROVE**` and
47
+ * `_VERDICT: APPROVE_` match (PR #636: the reviewer subagent emitted the
48
+ * bolded form and the gate read the PR as carrying no code-review verdict at
49
+ * all). The spec still mandates the bare line; this only stops an agent's
50
+ * markdown drift from silently DANGLING a real verdict.
51
+ *
52
+ * Emphasis ONLY — the following are deliberately NOT tolerated, because each
53
+ * is a way for prose to *mention* a verdict rather than *cast* one, and this
54
+ * gate blocks merges (#639 review, findings 1/3/5):
55
+ * `> VERDICT: APPROVE` blockquote — GitHub's quote-reply syntax. A
56
+ * Developer quoting the reviewer's earlier text
57
+ * would otherwise become the PR's own verdict, and
58
+ * most-recent-hit-wins means the quote beats a live
59
+ * REQUEST CHANGES. Fail-open and silent.
60
+ * `* VERDICT: APPROVE` list item — the space is what distinguishes it
61
+ * from emphasis; a bullet is prose context. `-` and
62
+ * `1.` never matched, so `*` must not either.
63
+ * `# VERDICT: APPROVE` heading — same mention-not-cast reasoning.
64
+ * `` `VERDICT: APPROVE` `` code span — decided, not overlooked. A
65
+ * backticked marker is exactly how the role docs
66
+ * and this comment WRITE about the contract, so
67
+ * tolerating it would match prose describing the
68
+ * rule. If a real agent ever emits the backticked
69
+ * form, fix the agent: it fails loud (DANGLING),
70
+ * which is the safe direction.
71
+ *
72
+ * The value-side boundary is `(?![A-Za-z0-9])`, not `\b`: `_` is a word
73
+ * character, so `\b` after the captured value would reject the closing `_` of
74
+ * `_VERDICT: APPROVE_` while accepting the `*` of the bolded form — the class
75
+ * and the claim would disagree. It still rejects `APPROVED`/`PASSED`.
76
+ *
77
+ * What is NOT loosened is the requirement that a literal `VERDICT:` token be
78
+ * present — ordinary prose and the Archivist's own DANGLING placeholder still
79
+ * miss.
80
+ *
81
+ * No separate "marker present but value unclear" branch: a generic
82
+ * `VERDICT:`-prefix-only marker would itself cross-contaminate the two
83
+ * extractors (a security reviewer's own `VERDICT: PASS` line would loosely
84
+ * match the code-review marker too, misreporting "code-reviewer comment
85
+ * found but unclear" for a comment that was never a code review at all).
86
+ * Anything that doesn't match the exact value pattern reads as "missing",
87
+ * identical to no comment existing — which is also what the DANGLING
88
+ * placeholder case above requires.
89
+ *
90
+ * Reviewed-commit binding (#73, a duplicate of #71 closes this one). The
91
+ * winning comment (the one whose `VERDICT:` line matched) is also searched
92
+ * for a `Judged head: <sha>` line — the exact phrase reviewers on
93
+ * atta-labs/attalabs#664 were already hand-typing before this existed as a
94
+ * mechanism, formalized rather than invented. Same anchor discipline as
95
+ * `VERDICT:` itself: line-start, optional leading emphasis run, no
96
+ * blockquote/list-item/heading/code-span tolerance — a sha mentioned in
97
+ * ordinary prose ("see 8365ca57 for context") is a mention, not a binding,
98
+ * and must not parse as one. Accepts both the abbreviated (7-char) and full
99
+ * (40-char) hex forms; `review-gate.ts` does the prefix comparison against
100
+ * the PR's actual head. Searched only within the SAME comment body that
101
+ * produced the winning verdict — a sha mentioned in a different comment is
102
+ * not this verdict's binding.
103
+ */
104
+
105
+ const HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im
106
+
107
+ /**
108
+ * Round-4 ruling on `#392`: both markers are read from a comment's first
109
+ * THREE lines only, never anywhere else in the body. Every shape this
110
+ * package itself renders (`review-post.ts`'s `renderCodeReviewComment`/
111
+ * `renderSecurityComment`/`renderEscalationComment`) puts `VERDICT:`/
112
+ * `ESCALATE:` on line 1 and `Judged head:` on line 3 — every caller-supplied
113
+ * field (findings, conformance prose, scope, a summary) renders strictly
114
+ * after that, starting at line 5 at the earliest. Restricting the window to
115
+ * lines 1–3 costs no real render anything: it is a strictly narrower read
116
+ * than "anywhere in the body," and it closes the class of defect that
117
+ * motivated this ruling — a caller-supplied field that smuggled a raw
118
+ * newline followed by a `VERDICT:`- or `Judged head:`-shaped line could
119
+ * previously inject a structural line from outside the command's own
120
+ * three-line skeleton; that line can now never be read as one.
121
+ */
122
+ function firstThreeLines(comment: string): string {
123
+ return comment.split('\n').slice(0, 3).join('\n')
124
+ }
125
+
126
+ function extractHeadSha(comment: string): string | null {
127
+ const m = firstThreeLines(comment).match(HEAD_SHA_PATTERN)
128
+ return m ? (m[1] as string).toLowerCase() : null
129
+ }
130
+
131
+ /**
132
+ * `headSha` is `null` in two distinct situations that both mean "cannot
133
+ * confirm this verdict covers the current head": no verdict comment matched
134
+ * at all (`danglingNote` is also set), or a verdict comment matched but
135
+ * carried no `Judged head:` line (`danglingNote` is `null` — the verdict
136
+ * itself is real, only the binding is missing). `review-gate.ts` treats both
137
+ * as unbound; only their `danglingNote`/`value` differ.
138
+ */
139
+ export type VerdictExtraction = { value: string; headSha: string | null; danglingNote: string | null }
140
+
141
+ /**
142
+ * Round 5 (`#392`): the candidate set — which comments even ATTEMPTED a
143
+ * verdict — is a whole-body test, the pre-round-4 pattern. Only the VALUE is
144
+ * read from the first three lines (round 4, kept). Round 4's own change
145
+ * built `clearHits` from the windowed text directly, which made a comment
146
+ * whose marker sits below line 3 vanish from consideration entirely rather
147
+ * than counting as present-but-unclear — "most recent clear hit wins" then
148
+ * silently fell through to an OLDER comment, so a later blocking verdict
149
+ * that happened to render past line 3 let an earlier clean pass win. Here,
150
+ * the most recent CANDIDATE is picked first; only then is its value read
151
+ * from its own first three lines. A candidate whose window carries no value
152
+ * is DANGLING — never skipped in favour of an earlier one — so a later
153
+ * unclear comment still shadows an earlier clean verdict, the fail-closed
154
+ * direction every other branch of this function already takes.
155
+ */
156
+ function extractVerdict(comments: string[], valuePattern: RegExp, missingLabel: string): VerdictExtraction {
157
+ const candidates = comments.filter((c) => valuePattern.test(c))
158
+ if (candidates.length === 0) {
159
+ return {
160
+ value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
161
+ headSha: null,
162
+ danglingNote: `no ${missingLabel} verdict comment found on this PR`
163
+ }
164
+ }
165
+
166
+ const latest = candidates[candidates.length - 1] as string
167
+ const m = firstThreeLines(latest).match(valuePattern)
168
+ if (!m) {
169
+ return {
170
+ value: `the most recent ${missingLabel} comment's VERDICT line is not within its first three lines — DANGLING, see below`,
171
+ headSha: null,
172
+ danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-three-line read window`
173
+ }
174
+ }
175
+
176
+ return {
177
+ value: (m[1] as string).toUpperCase().replace(/[_-]/g, ' '),
178
+ headSha: extractHeadSha(latest),
179
+ danglingNote: null
180
+ }
181
+ }
182
+
183
+ /** `value` is `APPROVE`, `REQUEST CHANGES`, `LGTM`, or a DANGLING placeholder string. */
184
+ export function extractCodeReviewVerdict(comments: string[]): VerdictExtraction {
185
+ return extractVerdict(
186
+ comments,
187
+ /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im,
188
+ 'code-reviewer'
189
+ )
190
+ }
191
+
192
+ /** `value` is `PASS`, `FAIL`, or a DANGLING placeholder string. */
193
+ export function extractSecurityReviewVerdict(comments: string[]): VerdictExtraction {
194
+ return extractVerdict(
195
+ comments,
196
+ /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im,
197
+ 'security-review'
198
+ )
199
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * vocabulary-citation.ts — a generic, regex-parameterized citation checker.
3
+ *
4
+ * `retired-vocabulary.test.ts` hand-rolls this exact sweep shape (pattern +
5
+ * scope + exemptions + a non-vacuity self-test) for this repo's own internal
6
+ * use, but as a `vitest` file no adopter can install it — and it hardcodes
7
+ * every pattern, scope and exemption as a repo-specific literal. This module
8
+ * generalizes the sweep into a reusable primitive: every pattern, its scope,
9
+ * its exemptions, and the sample that proves it is not vacuous arrive as
10
+ * caller-supplied input. It contains no product name, host-tool path, or
11
+ * any other repo-specific literal — that knowledge belongs to whoever
12
+ * configures an instance (a Vinaya check bin is one such consumer).
13
+ *
14
+ * Pure — no `fs`, no child-process I/O. The caller injects `grepFn`/
15
+ * `matchFn`, mirroring `evaluateC5`'s `fileExists` injection in
16
+ * `doc-owners.ts`: this file decides what counts as a finding, the I/O shim
17
+ * decides how to look.
18
+ */
19
+
20
+ export type VocabularyPattern = {
21
+ /** Short, stable id for reporting — never the raw regex, which can be long. */
22
+ id: string
23
+ /**
24
+ * POSIX ERE, `grep -E` compatible. Never `\d`/`\w`/`\s` — GNU grep reads
25
+ * those as literal characters, not shorthand classes, so a pattern relying
26
+ * on them can pass locally (a permissive grep) and go silently vacuous on
27
+ * CI (GNU grep). Use `[0-9]` etc. instead. `\b` is safe: both GNU and BSD
28
+ * `grep -E` support it as an extension.
29
+ */
30
+ pattern: string
31
+ /** Repo-root-relative paths/dirs this pattern is checked against. */
32
+ scope: string[]
33
+ /** Path substrings exempted for this pattern only, beyond `globalExempt`. */
34
+ exempt?: string[]
35
+ /**
36
+ * Substrings that, when present on the matched LINE, make the hit legitimate.
37
+ * Distinct from `exempt`, which filters by path: a name can be leakage in one
38
+ * sentence and the product's own identifier in the next, within the same file.
39
+ * An npm scope or a domain that merely CONTAINS a product name is the product
40
+ * naming itself, not an unexplained reference an adopter cannot resolve —
41
+ * exempting the whole file would blind the pattern to real leakage beside it.
42
+ *
43
+ * Granularity is the LINE, which is the accepted limit: a genuine leak that
44
+ * shares a line with a legitimate citation escapes. Prose puts them on
45
+ * separate lines in practice, and the alternative — exempting only the
46
+ * matched span — would need column offsets `grep -n` does not report.
47
+ */
48
+ exemptContent?: string[]
49
+ /** A string this pattern MUST match — proves the pattern is not vacuous. */
50
+ sample: string
51
+ /**
52
+ * When true, a hit on a line that is a markdown table row (starts with
53
+ * `|`, ignoring leading whitespace) is not a finding. A registry table's
54
+ * "implementation" column citing a harness path is verified by a gate
55
+ * (e.g. `verify-registry.ts`'s G1/G2), not a claim stated as normative
56
+ * prose — banning the shape would flag the very registry a widened
57
+ * pattern exists to protect.
58
+ */
59
+ exemptTableRow?: boolean
60
+ }
61
+
62
+ export type VocabularyHit = { file: string; line: number; content: string }
63
+
64
+ export type VocabularyFinding = VocabularyHit & { patternId: string }
65
+
66
+ export type VocabularyCheckResult = {
67
+ findings: VocabularyFinding[]
68
+ /**
69
+ * Patterns whose sample failed to self-match — a config bug, reported
70
+ * loud instead of silently passing. A pattern that matches nothing passes
71
+ * every scope silently, which is not hypothetical: this is the exact
72
+ * failure mode `retired-vocabulary.test.ts`'s own non-vacuity suite exists
73
+ * to catch (`\bD-(###|NNN)\b` shipped green over eleven live sites because
74
+ * `\b` cannot anchor after `#`).
75
+ */
76
+ vacuousPatterns: string[]
77
+ }
78
+
79
+ const TABLE_ROW = /^\s*\|/
80
+
81
+ /**
82
+ * Evaluates every pattern against its own scope, filtering exempted paths
83
+ * and (optionally) table-row citations, and reports which patterns proved
84
+ * vacuous against their own sample instead of silently returning clean.
85
+ */
86
+ export function evaluateVocabularyCitation(
87
+ patterns: VocabularyPattern[],
88
+ grepFn: (pattern: string, scope: string[]) => VocabularyHit[],
89
+ matchFn: (pattern: string, sample: string) => boolean,
90
+ globalExempt: string[] = []
91
+ ): VocabularyCheckResult {
92
+ const findings: VocabularyFinding[] = []
93
+ const vacuousPatterns: string[] = []
94
+
95
+ for (const p of patterns) {
96
+ if (!matchFn(p.pattern, p.sample)) {
97
+ vacuousPatterns.push(p.id)
98
+ continue
99
+ }
100
+
101
+ const exempt = [...globalExempt, ...(p.exempt ?? [])]
102
+ const exemptContent = p.exemptContent ?? []
103
+ const hits = grepFn(p.pattern, p.scope).filter((h) => {
104
+ if (exempt.some((e) => h.file.includes(e))) return false
105
+ if (exemptContent.some((e) => h.content.includes(e))) return false
106
+ if (p.exemptTableRow && TABLE_ROW.test(h.content)) return false
107
+ return true
108
+ })
109
+
110
+ for (const h of hits) findings.push({ ...h, patternId: p.id })
111
+ }
112
+
113
+ return { findings, vacuousPatterns }
114
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Waiver-label actor verification. A doc-coverage waiver is honored
3
+ * ONLY when the `vinaya/waiver:docs` PR label is present AND the actor of that
4
+ * label's most recent labeling timeline event is a configured principal.
5
+ * Label presence alone is never sufficient — that is exactly the
6
+ * agent-emittable-string hole closes (supersedes's
7
+ * `Doc-waiver:` grammar). Pure — no env reads here; I/O stays in the CLI
8
+ * shim, same discipline as `pr-tier.ts`.
9
+ *
10
+ * `isWaiverLabelActorVerified` is parameterized by `label` (aeg-review-gate-v1
11
+ * task 1, #474) so a second waiver label (`vinaya/waiver:review`) can reuse the exact
12
+ * same actor-verification predicate rather than a copy-pasted duplicate that
13
+ * could drift out of sync.
14
+ *
15
+ * The label strings themselves are read from the code-owned vocabulary in
16
+ * `@attalabs/aeg-forge-state`'s `labels.ts`, never written as literals here — a
17
+ * namespace change lands in one file (discipline).
18
+ */
19
+
20
+ import { label as labelFor } from '@attalabs/aeg-forge-state'
21
+
22
+ export const WAIVER_LABEL = labelFor('waiver-docs')
23
+ export const WAIVER_LABEL_REVIEW = labelFor('waiver-review')
24
+ export const PRINCIPAL_ALLOWLIST = ['daniboomerang']
25
+
26
+ /**
27
+ * Membership test for a principal allowlist. **Case-insensitive, because
28
+ * GitHub logins are** — `Alice` and `alice` address the same account, and the
29
+ * forge may return either casing depending on the API surface. An exact-match
30
+ * test fails closed, but it fails closed *invisibly*: an adopter who wrote
31
+ * `"principals": ["Alice"]` would watch Alice's genuine verdicts land as
32
+ * DANGLING with no stated reason — precisely the baffling symptom the
33
+ * `principals` field exists to remove (review finding, PR #862).
34
+ *
35
+ * Shared by every allowlist consumer so the waiver check and the review gate
36
+ * can never disagree about whether a given login is a principal.
37
+ */
38
+ export function isPrincipal(login: string | null, principalAllowlist: string[]): boolean {
39
+ if (login === null) return false
40
+ const normalized = login.toLowerCase()
41
+ return principalAllowlist.some((p) => p.toLowerCase() === normalized)
42
+ }
43
+
44
+ export function isWaiverLabelActorVerified(opts: {
45
+ label: string
46
+ labels: string[]
47
+ labelActor: string | null
48
+ principalAllowlist: string[]
49
+ }): boolean {
50
+ if (!opts.labels.includes(opts.label)) return false
51
+ return isPrincipal(opts.labelActor, opts.principalAllowlist)
52
+ }