@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,94 @@
1
+ /**
2
+ * Doctrine-no-procedures sweep (task 10, Issue #385) — task 9's rule that
3
+ * doctrine prose describes no command sequence, made a check. `roles/*.md`
4
+ * and `contracts/*.md` explain *what* a command sequence does and *why*;
5
+ * they are not a runbook a reader executes verbatim, and a copy-pasted
6
+ * sequence rots the moment the real command changes (found live: two of
7
+ * this tranche's own PRs were blocked writing exactly this shape after the
8
+ * rule was already known in prose).
9
+ *
10
+ * Zero I/O: every input (file paths + contents) is read by the adapter
11
+ * (`check-doctrine-no-procedures.ts`) and passed in — same discipline as
12
+ * `doctrine-portability.ts` beside it.
13
+ */
14
+
15
+ import { COMMAND_WORDS, extractFencedBlocks } from './brief-validation'
16
+ import { VENDOR_EXAMPLE_END, VENDOR_EXAMPLE_START } from './doctrine-portability'
17
+
18
+ export type DoctrineFile = { path: string; content: string }
19
+ export type DoctrineProcedureFinding = { file: string; line: number; message: string }
20
+
21
+ /**
22
+ * The one sanctioned fenced home for a real command sequence in doctrine
23
+ * (`tranche-model.md` §12's `AEG:VENDOR-EXAMPLE` pair) — same markers
24
+ * `doctrine-portability.ts` exempts from its own vendor-name scan, exported
25
+ * from there rather than re-declared here. First pair wins, same rule as
26
+ * every other `AEG:*` anchor consumer.
27
+ */
28
+ function vendorExampleSpan(content: string): { start: number; end: number } | null {
29
+ const start = VENDOR_EXAMPLE_START.exec(content)
30
+ if (!start) return null
31
+ const afterStart = start.index + start[0].length
32
+ const end = VENDOR_EXAMPLE_END.exec(content.slice(afterStart))
33
+ if (!end) return null
34
+ return { start: start.index, end: afterStart + end.index + end[0].length }
35
+ }
36
+
37
+ function isCommandLine(line: string): boolean {
38
+ const word = line.trim().split(/\s+/)[0] ?? ''
39
+ return (COMMAND_WORDS as readonly string[]).includes(word)
40
+ }
41
+
42
+ /**
43
+ * A fence's language tag that reads as shell — the empty tag (` ``` `, most
44
+ * doctrine command blocks) or an explicit `sh`/`bash`/`shell`/`console`.
45
+ * Anything else (`ts`, `js`, `yaml`, `json`, …) is a real illustration in
46
+ * that language, not a command sequence — a two-line ` ```ts ` block with two
47
+ * `export` statements is TypeScript, not shell, even though `export` is also
48
+ * a shell builtin (round-2 ruling item 2, found live: this task's own
49
+ * `brief-validation.ts` excerpt in `state-machine.md` reads exactly this
50
+ * way). A block outside this set never reaches the command-line count at
51
+ * all — it is not a candidate, regardless of its content.
52
+ */
53
+ const SHELL_LANGS: ReadonlySet<string> = new Set(['', 'sh', 'bash', 'shell', 'console'])
54
+
55
+ function procedureLineNumberAt(content: string, index: number): number {
56
+ return content.slice(0, index).split('\n').length
57
+ }
58
+
59
+ /** `aeg-root/templates/**` (or any adopter's `<doctrineRoot>/templates/**`) — worked-example templates that show the vendor-facing PR-report shape and legitimately carry a full command block. */
60
+ function isTemplatePath(path: string): boolean {
61
+ return /(^|\/)templates\//.test(path)
62
+ }
63
+
64
+ /**
65
+ * A fenced block containing two or more lines that each start with a shell
66
+ * command word is a command sequence, not an illustration — the failure
67
+ * names it: "this sequence is a `vinaya` command, name it." Exempt: a block
68
+ * inside the `AEG:VENDOR-EXAMPLE` anchor pair, and any file under a
69
+ * `templates/` directory.
70
+ */
71
+ export function checkDoctrineNoProcedures(files: DoctrineFile[]): DoctrineProcedureFinding[] {
72
+ const findings: DoctrineProcedureFinding[] = []
73
+
74
+ for (const file of files) {
75
+ if (isTemplatePath(file.path)) continue
76
+ const span = vendorExampleSpan(file.content)
77
+
78
+ for (const block of extractFencedBlocks(file.content)) {
79
+ if (span && block.start >= span.start && block.end <= span.end) continue
80
+ if (!SHELL_LANGS.has(block.lang)) continue
81
+
82
+ const commandLineCount = block.content.split('\n').filter(isCommandLine).length
83
+ if (commandLineCount >= 2) {
84
+ findings.push({
85
+ file: file.path,
86
+ line: procedureLineNumberAt(file.content, block.start),
87
+ message: `this fenced block has ${commandLineCount} shell-command lines — this sequence is a \`vinaya\` command, name it (or move it inside the \`AEG:VENDOR-EXAMPLE\` anchor / a \`templates/\` file).`
88
+ })
89
+ }
90
+ }
91
+ }
92
+
93
+ return findings
94
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Doctrine portability — the class `check-reader-resolvable-prose` sweeps
3
+ * but cannot see (task 234, Issue #234). That check's own module header
4
+ * declares zero I/O, so it never resolves a cited path against a
5
+ * filesystem — and if it tried, it would resolve against the authoring
6
+ * repo, the one environment where every author-repo-internal path in
7
+ * `aeg-root/**` happens to exist.
8
+ *
9
+ * The reframe that makes this decidable with no I/O at all: the rule is not
10
+ * "this path does not resolve in the adopter" (which needs a filesystem to
11
+ * even ask) but **"portable doctrine names a non-portable path."** A cited
12
+ * path is judged purely by its own shape — its top path segment — against a
13
+ * fixed allow-list of prefixes known to be doctrine-relative or
14
+ * adopter-owned. Everything else is a finding, including a prefix nobody
15
+ * has classified yet: an allow-list fails closed on an unknown prefix (a
16
+ * build artifact is exactly as non-portable as the source it was built
17
+ * from), where a deny-list of "known author-repo prefixes" would fail open
18
+ * on the next unlisted one instead.
19
+ *
20
+ * **A second, additive dimension (Issue #298): a portable path is not the
21
+ * only way doctrine couples itself to one vendor.** Prose can name a
22
+ * specific AI company, product, or agent directly — "Claude Code", "GPT",
23
+ * "Anthropic" — with no path shape for the path-based predicate above to
24
+ * even look at. `checkDoctrinePortability` now emits a SECOND finding kind
25
+ * for exactly that: a fixed vendor-name word list, scanned against prose
26
+ * (never against code — a masked, `maskCode`-blind scan, the identical
27
+ * code-recognition grammar `anchored-region.ts` uses for the `AEG:*` PR/Issue
28
+ * body anchors, imported directly rather than re-implemented), with exactly
29
+ * ONE standing exemption: text inside a `<!-- AEG:VENDOR-EXAMPLE:START -->` /
30
+ * `<!-- AEG:VENDOR-EXAMPLE:END -->` pair — doctrine's one sanctioned, fenced
31
+ * home for naming today's shipped reference host by product name
32
+ * (`tranche-model.md` §12). A vendor word that is itself part of an
33
+ * already-portable path citation (`` `.claude/hooks/x.sh` ``, `` `CLAUDE.md` ``)
34
+ * is not re-flagged here: it is inline code, already masked by `maskCode`
35
+ * before the word scan ever runs, and the path-shape predicate above already
36
+ * has an opinion about it. This dimension has no I/O of its own either — same
37
+ * inputs, same adapter.
38
+ *
39
+ * Zero I/O: every input (file paths + contents) is read by the adapter and
40
+ * passed in.
41
+ */
42
+
43
+ import { maskCode } from '@attalabs/aeg-forge-state/strip-code'
44
+
45
+ export type PortabilitySourceFile = { path: string; content: string }
46
+
47
+ export type PortabilityFinding = {
48
+ file: string
49
+ line: number
50
+ cited: string
51
+ message: string
52
+ /** `'path'` — the original non-portable-path predicate. `'vendor-name'` — Issue #298's word-list predicate. */
53
+ kind: 'path' | 'vendor-name'
54
+ }
55
+
56
+ /** `aeg-root/**` by default — what this repo's own package ships and every adopter installs read-only. */
57
+ const DEFAULT_SHIPS_PREFIX = 'aeg-root/'
58
+
59
+ /**
60
+ * Doctrine-relative (`roles/`, `contracts/`, `skills/`) and adopter-owned
61
+ * (`.github/`, `.vinaya/`, `.claude/`) top segments — the two portable
62
+ * classes measured in the task-234 corpus scan. `aeg-root/` itself is
63
+ * deliberately NOT a static entry here: it is always the CALLER-supplied
64
+ * `shipsPrefix`, checked dynamically in `isPortable` below, so an adopter
65
+ * who configures a non-default `doctrineRoot` gets that root treated as
66
+ * portable too — a static `'aeg-root/'` entry would falsely flag every one
67
+ * of that adopter's own self-citations as non-portable. A citation whose
68
+ * top segment falls outside this list (and isn't the ships prefix) is
69
+ * never assumed portable, however plausible it looks; it is a finding, and
70
+ * someone extends this list deliberately once its side of the
71
+ * classification is decided.
72
+ */
73
+ const STATIC_PORTABLE_PREFIXES: readonly string[] = [
74
+ 'roles/',
75
+ 'contracts/',
76
+ 'skills/',
77
+ '.github/',
78
+ '.vinaya/',
79
+ '.claude/',
80
+ // Not in the task-234 corpus-scan table, but verified while reading the
81
+ // corpus: `.git/hooks/*` and `.husky/*` are cited only as the pre-commit/
82
+ // pre-push hook locations every git repo (and every husky-adopting repo)
83
+ // genuinely has — adopter-owned by construction, the same class as
84
+ // `.github/`/`.vinaya/`/`.claude/` above.
85
+ '.git/',
86
+ '.husky/'
87
+ ]
88
+
89
+ /**
90
+ * The three illustrative placeholders measured in the corpus — a
91
+ * `[path/inside/the/...]`-shaped fill-in-the-blank in a template, never a
92
+ * real citation. Exempted by literal, not by pattern, so a real path that
93
+ * happens to share a prefix with one of these is never accidentally waved
94
+ * through.
95
+ */
96
+ const EXEMPT_LITERALS: ReadonlySet<string> = new Set([
97
+ 'path/inside/the/shipped/diff.ts',
98
+ 'path/inside/the/surface.ts',
99
+ 'apps/x/specs/...'
100
+ ])
101
+
102
+ /**
103
+ * A cited path, inline-backtick-delimited, shaped like a repo-relative
104
+ * path: starts with a word/dot/dash character (never `/` — that shape is a
105
+ * web route, e.g. `` `/docs/state-machine` ``, not a repo path), contains at
106
+ * least one `/`, and carries no character a URL or a prose fragment would
107
+ * (no `:`, no whitespace, no parens) — which also excludes a link like
108
+ * `` `https://vinaya.dev` `` without a separate URL-shaped exclusion.
109
+ */
110
+ const CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/
111
+
112
+ /**
113
+ * Top segments that are never a repo path, verified against every one of
114
+ * their occurrences in the real corpus rather than assumed: `origin/`/`refs/`
115
+ * are git-ref namespace (`origin/main`, `refs/pull/N/merge`), and `vinaya/`/
116
+ * `fix/` are this doctrine's own GitHub-label and branch-name-example
117
+ * conventions (`vinaya/blocked`, `fix/brief-gate-nontask`) — a citation kind
118
+ * this check does not judge at all, portable or not, the same way it never
119
+ * tries to also judge a forge number or a tranche slug.
120
+ *
121
+ * **A closed, tested set, not a growable escape hatch.** Excluding a top
122
+ * segment here means every citation under it is invisible to this check —
123
+ * never flagged, however non-portable it would otherwise be — so this set
124
+ * carries exactly the corpus-verified exceptions above and nothing else.
125
+ * `doctrine-portability.test.ts` locks its exact membership; an addition
126
+ * that doesn't also update that lock is a bug, not a silent expansion.
127
+ * Exported for that lock, not for callers to extend at runtime.
128
+ */
129
+ export const NON_PATH_TOP_SEGMENTS: ReadonlySet<string> = new Set(['origin', 'refs', 'HEAD', 'vinaya', 'fix'])
130
+
131
+ /**
132
+ * The vendor-name word list (Issue #298's re-count, live at authoring):
133
+ * every AI company/product/agent name this doctrine's own prose has
134
+ * actually used, as either the shipped reference host or a peer example —
135
+ * a real, hardcoded list for this repo's own corpus, not a growable
136
+ * config surface (brief `fix/doctrine-vendor-neutrality` §10: ship a
137
+ * reasonable hardcoded list, flag generalization for later). Longest
138
+ * alternative first ("claude code" before "claude") so a two-word mention
139
+ * is reported whole rather than as a truncated single-word match followed
140
+ * by a dangling "code".
141
+ *
142
+ * Includes model-TIER names (`opus`/`sonnet`/`haiku`), not just
143
+ * company/product names — a review round on this task's own PR (#338)
144
+ * found the first cut missed exactly this class: `brief-authoring/SKILL.md`
145
+ * and `brief-template.md` named a specific model tier bare, in prose, in
146
+ * three places the path-shape predicate could never see either. `sonnet`
147
+ * and `haiku` are ordinary English words outside this domain — a real
148
+ * false-positive risk for a general-purpose tool, accepted here on the
149
+ * same "reasonable hardcoded list for this repo's own corpus" basis as the
150
+ * rest of this list (verified against the live corpus: no non-vendor use
151
+ * of either word exists in `aeg-root/**` today).
152
+ */
153
+ const VENDOR_NAME_SOURCE =
154
+ '\\bclaude code\\b|\\bclaude\\b|\\banthropic\\b|\\bchatgpt\\b|\\bopenai\\b|\\bgpt\\b|\\bgemini\\b|\\bcodex\\b|\\bgrok\\b|\\bdeepseek\\b|\\bopus\\b|\\bsonnet\\b|\\bhaiku\\b'
155
+
156
+ /**
157
+ * Exported (alongside `VENDOR_EXAMPLE_END` below) so `doctrine-no-procedures.ts`
158
+ * can find the same fenced home's raw-text span — never a second copy of this
159
+ * pattern — to exempt a fenced block that sits inside it, the same way this
160
+ * file exempts the region from its own vendor-name word scan.
161
+ */
162
+ export const VENDOR_EXAMPLE_START = /<!--\s*AEG:VENDOR-EXAMPLE:START\s*-->/
163
+ export const VENDOR_EXAMPLE_END = /<!--\s*AEG:VENDOR-EXAMPLE:END\s*-->/
164
+
165
+ /**
166
+ * Blanks (same-length, index-preserving — same discipline as `maskCode`
167
+ * itself) the region between the first well-formed
168
+ * `<!-- AEG:VENDOR-EXAMPLE:START -->` … `<!-- AEG:VENDOR-EXAMPLE:END -->`
169
+ * pair in `masked` — doctrine's one sanctioned home for naming today's
170
+ * shipped reference host by product name (`tranche-model.md` §12).
171
+ * Markers are searched on already-`maskCode`d text, the same order
172
+ * `anchoredRegionBounds` searches the `AEG:*` PR/Issue-body anchors, so a
173
+ * decoy pair quoted inside a fenced example never wins. A START with no
174
+ * following END is not a fence at all — the same "malformed half-pair is no
175
+ * anchor" rule `anchored-region.ts` applies.
176
+ *
177
+ * **First pair wins, same as `anchoredRegionBounds` — a SECOND pair in the
178
+ * same file is not masked.** By design there is exactly one fenced home in
179
+ * the whole doctrine tree (the Goal this check exists to hold), so a
180
+ * second pair anywhere is itself a doctrine defect, not a shape this
181
+ * function needs to accommodate; scanning per-file rather than per-pair
182
+ * keeps that failure visible (a second, unmasked pair still reports its
183
+ * own `vendor-name` findings) instead of silently exempting it too.
184
+ */
185
+ function maskVendorExampleRegion(masked: string): string {
186
+ const start = VENDOR_EXAMPLE_START.exec(masked)
187
+ if (!start) return masked
188
+ const afterStart = start.index + start[0].length
189
+ const end = VENDOR_EXAMPLE_END.exec(masked.slice(afterStart))
190
+ if (!end) return masked
191
+ const regionEnd = afterStart + end.index + end[0].length
192
+ const region = masked.slice(start.index, regionEnd)
193
+ const blanked = region.replace(/[^\n]/g, ' ')
194
+ return masked.slice(0, start.index) + blanked + masked.slice(regionEnd)
195
+ }
196
+
197
+ /**
198
+ * Every vendor-name-list match in `content`, code-blind (`maskCode`, the
199
+ * same grammar `anchored-region.ts` uses) and blind to the one fenced
200
+ * `AEG:VENDOR-EXAMPLE` home. A vendor word that is only part of an
201
+ * already-portable inline-code path citation (`` `.claude/hooks/x.sh` ``,
202
+ * `` `CLAUDE.md` ``) never reaches this scan at all — it is masked before
203
+ * the word list ever runs, the same way a fenced worked example is blind to
204
+ * `body-bare-digits`.
205
+ */
206
+ function extractVendorMentions(content: string): { name: string; index: number }[] {
207
+ const scoped = maskVendorExampleRegion(maskCode(content))
208
+ const found: { name: string; index: number }[] = []
209
+ const pattern = new RegExp(VENDOR_NAME_SOURCE, 'gi')
210
+ let match: RegExpExecArray | null = pattern.exec(scoped)
211
+ while (match !== null) {
212
+ found.push({ name: match[0], index: match.index })
213
+ match = pattern.exec(scoped)
214
+ }
215
+ return found
216
+ }
217
+
218
+ /** Every inline-backtick span in `content`, tested against `CITED_PATH_PATTERN`. */
219
+ function extractCitedPaths(content: string): { cited: string; index: number }[] {
220
+ const found: { cited: string; index: number }[] = []
221
+ const spanPattern = /`([^`\n]+)`/g
222
+ let match: RegExpExecArray | null = spanPattern.exec(content)
223
+ while (match !== null) {
224
+ const cited = match[1] ?? ''
225
+ const topSegment = cited.slice(0, cited.indexOf('/'))
226
+ if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
227
+ found.push({ cited, index: match.index })
228
+ }
229
+ match = spanPattern.exec(content)
230
+ }
231
+ return found
232
+ }
233
+
234
+ function lineAtIndex(content: string, index: number): number {
235
+ let line = 1
236
+ for (let i = 0; i < index; i++) {
237
+ if (content.charCodeAt(i) === 10) line++
238
+ }
239
+ return line
240
+ }
241
+
242
+ /**
243
+ * True iff `cited`'s top segment is portable: the caller-supplied
244
+ * `shipsPrefix` itself (doctrine citing its own tree, wherever that tree
245
+ * actually lives for this caller), or one of the static portable prefixes.
246
+ */
247
+ function isPortable(cited: string, shipsPrefix: string): boolean {
248
+ if (cited.startsWith(shipsPrefix)) return true
249
+ return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix))
250
+ }
251
+
252
+ /**
253
+ * Sweeps every file under `shipsPrefix` for two independent finding kinds:
254
+ * a cited path whose top segment is not allow-listed portable (`'path'`),
255
+ * and a vendor-name-list word used in prose outside the one fenced
256
+ * `AEG:VENDOR-EXAMPLE` home (`'vendor-name'`, Issue #298). Files outside
257
+ * `shipsPrefix` are out of scope entirely — this check only judges what the
258
+ * shipped doctrine tree itself cites/names, never a repo's other source.
259
+ */
260
+ export function checkDoctrinePortability(
261
+ files: readonly PortabilitySourceFile[],
262
+ shipsPrefix: string = DEFAULT_SHIPS_PREFIX
263
+ ): PortabilityFinding[] {
264
+ const findings: PortabilityFinding[] = []
265
+
266
+ for (const file of files) {
267
+ if (!file.path.startsWith(shipsPrefix)) continue
268
+
269
+ for (const { cited, index } of extractCitedPaths(file.content)) {
270
+ if (EXEMPT_LITERALS.has(cited)) continue
271
+ if (isPortable(cited, shipsPrefix)) continue
272
+ findings.push({
273
+ file: file.path,
274
+ line: lineAtIndex(file.content, index),
275
+ cited,
276
+ message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`,
277
+ kind: 'path'
278
+ })
279
+ }
280
+
281
+ for (const { name, index } of extractVendorMentions(file.content)) {
282
+ findings.push({
283
+ file: file.path,
284
+ line: lineAtIndex(file.content, index),
285
+ cited: name,
286
+ message: `names "${name}" outside the one fenced vendor-example home (an <!-- AEG:VENDOR-EXAMPLE:START --> … <!-- AEG:VENDOR-EXAMPLE:END --> pair) — portable doctrine refers to a host generically everywhere else`,
287
+ kind: 'vendor-name'
288
+ })
289
+ }
290
+ }
291
+
292
+ return findings
293
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The seam the pure derivation (`deriveDiagramModel`) consumes doctrine
3
+ * through, instead of reading `aeg-root/` paths directly. Doctrine is the raw
4
+ * markdown that governs the methodology — `enforcement.md`, the role files,
5
+ * the contract files. Implementations (file-backed today, package-bundled for
6
+ * adopters tomorrow) perform I/O and therefore live outside `aeg-core`
7
+ * (`apps/vinaya/sources`) — this package only defines the contract, so the
8
+ * library can be packaged for repos that have no `aeg-root/` of their own
9
+ *. Same discipline as `state-source.ts`: zero I/O here, async on the
10
+ * adapter so callers get one uniform type regardless of the backing store.
11
+ */
12
+ export type DoctrineContent = {
13
+ /** Raw `enforcement.md` content. */
14
+ enforcement: string
15
+ /** Raw `roles/*.md` files (path + content). */
16
+ roles: Array<{ path: string; content: string }>
17
+ /** Raw `contracts/*.md` files (path + content). */
18
+ contracts: Array<{ path: string; content: string }>
19
+ }
20
+
21
+ export type DoctrineSource = {
22
+ getDoctrine(): Promise<DoctrineContent>
23
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Idempotent label-minting (vinaya-label-hygiene-v1 task 1, #243) — the
3
+ * decision logic three call sites (`apps/cli/src/commands/audit.ts`,
4
+ * `bin/check-direct-main-push.ts`, `bin/dead-branch-audit.ts`) each
5
+ * reimplemented: does this label already exist on the forge, and if not,
6
+ * create it. Pure — no `gh` shell-outs here; each call site injects its own
7
+ * `listLabelNames`/`createLabel`, so this module stays agnostic to how a
8
+ * caller shells out (array-arg `execFileSync` vs. string `execSync`) and to
9
+ * whether a caller lets `gh` failures throw or swallows them — those are
10
+ * call-site error-handling policies this extraction does not change.
11
+ *
12
+ * Mints lazily, on first use, by design — never called to bootstrap the
13
+ * full label vocabulary (`@attalabs/aeg-forge-state`'s `labels.ts`) ahead of
14
+ * need.
15
+ */
16
+
17
+ export type LabelExistenceIo = {
18
+ listLabelNames: (repoFlag: string) => string[]
19
+ createLabel: (repoFlag: string, name: string, description: string, color: string) => void
20
+ }
21
+
22
+ export const LABEL_COLOR = 'B60205'
23
+
24
+ export function ensureLabelExists(repoFlag: string, name: string, description: string, io: LabelExistenceIo): void {
25
+ const existing = io.listLabelNames(repoFlag)
26
+ if (existing.includes(name)) return
27
+ io.createLabel(repoFlag, name, description, LABEL_COLOR)
28
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Path classifiers shared by the tier-derivation and doc-owners checks.
3
+ * Pure string predicates — no I/O.
4
+ */
5
+
6
+ /**
7
+ * Decision archives: frozen records of what was decided, not documentation.
8
+ * Touching one must never satisfy C3's code-requires-docs pairing, carry a
9
+ * tier signal, or be held to a spec's `Status:` block.
10
+ *
11
+ * Recognized by filename suffix, never by an enumerated path list. An earlier
12
+ * form of this predicate hardcoded each consumer's archive by full path, which
13
+ * put specific product names inside a package that ships to adopters who have
14
+ * neither. The suffix is the actual rule — a file named `…decisions-legacy.md`
15
+ * is an archive in any repo — and it subsumes every path that list held, so
16
+ * the coupling was removable with no behavior change.
17
+ *
18
+ * The historical failure the path list was added to prevent (renaming an
19
+ * archive out of recognition) is not re-opened: the earlier rule required a
20
+ * `-decisions-legacy.md` suffix and so missed a top-level `decisions-legacy.md`
21
+ * with no prefix, which is why the list existed at all. Matching the bare
22
+ * suffix covers both shapes.
23
+ */
24
+ export function isFrozenArchive(p: string): boolean {
25
+ return p.endsWith('decisions-legacy.md')
26
+ }
27
+
28
+ export function isDocFile(p: string): boolean {
29
+ return (
30
+ (p.startsWith('aeg-root/') && p.endsWith('.md')) ||
31
+ (p.startsWith('aeg-project/') && p.endsWith('.md')) ||
32
+ (p.includes('/aeg-project/') && p.endsWith('.md')) ||
33
+ (p.startsWith('.vinaya/') && p.endsWith('.md')) ||
34
+ (p.startsWith('docs/') && p.endsWith('.md') && !isFrozenArchive(p)) ||
35
+ (p.startsWith('apps/') && p.includes('/specs/') && p.endsWith('.md') && !isFrozenArchive(p)) ||
36
+ (p.startsWith('.claude/skills/') && p.endsWith('.md')) ||
37
+ /^(apps|packages)\/[^/]+\/README\.md$/.test(p) ||
38
+ p === 'docs-index.md' ||
39
+ p === 'README.md' ||
40
+ p === 'CLAUDE.md'
41
+ )
42
+ }
43
+
44
+ export function isCodeFile(p: string): boolean {
45
+ if (p.endsWith('.md')) return false
46
+ return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p)
47
+ }
48
+
49
+ export function isSpecFile(p: string): boolean {
50
+ return p.startsWith('apps/') && p.includes('/specs/') && p.endsWith('.md') && !isFrozenArchive(p)
51
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * First-push dispatch gate (aeg-governance-hardening task 25, #365). Pure —
3
+ * no `fs`, no `gh`/`git` shell-outs. The CLI shim (`bin/check-first-push-
4
+ * dispatch.ts`, wired into `.husky/pre-push`) runs the UNCHANGED
5
+ * `verify-dispatch.ts` gate mode once on a `task/<tranche>/<n>` branch's
6
+ * first push, maps its exit/output to a `DispatchReadinessFact`, and passes
7
+ * it in here.
8
+ *
9
+ * Mechanizes `roles/developer.md`'s entry-gate items 3–7 at the earliest
10
+ * possible moment — the first push — instead of relying on the Developer to
11
+ * run `verify-dispatch.ts` by hand before Step 0. Subsequent pushes (once a
12
+ * PR exists) skip the gate: dispatch readiness was validated once, and
13
+ * re-blocking mid-task on a sibling's later state change would strand
14
+ * in-flight work.
15
+ *
16
+ * `UNKNOWN` (forge unreachable — `verify-dispatch.ts`'s own `severity:infra`
17
+ * marker, printed when repo/token resolution fails) maps to `allow`, the
18
+ * same deliberate fail-open choice `dead-branch-push-guard.ts` already makes
19
+ * for the sibling gate: a guard that can block *all* pushes on a
20
+ * transient forge-reachability issue is worse than the bug it fixes. See
21
+ * `aeg-root/enforcement.md`.
22
+ */
23
+
24
+ /** A `task/<tranche>/<n>` branch parses to its two topology coordinates; anything else does not. */
25
+ export function parseTaskBranch(branch: string): { tranche: string; taskId: string } | null {
26
+ const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch)
27
+ return m ? { tranche: m[1] as string, taskId: m[2] as string } : null
28
+ }
29
+
30
+ export type DispatchReadinessFact = 'READY' | 'NOT_READY' | 'UNKNOWN'
31
+
32
+ export type FirstPushDispatchGateInput = {
33
+ /** The branch being pushed. */
34
+ branch: string
35
+ /** Whether a PR already exists for this branch — the hook's existing C5/ detection, reused, never re-derived here. */
36
+ prExists: boolean
37
+ /** verify-dispatch's classified result. Irrelevant (never read) when the branch is not a task branch or a PR already exists. */
38
+ readiness: DispatchReadinessFact
39
+ }
40
+
41
+ export type FirstPushDispatchGateVerdict = 'allow' | 'refuse'
42
+
43
+ export type FirstPushDispatchGateResult = { verdict: FirstPushDispatchGateVerdict; reason: string }
44
+
45
+ export function checkFirstPushDispatchGate(input: FirstPushDispatchGateInput): FirstPushDispatchGateResult {
46
+ const { branch, prExists, readiness } = input
47
+
48
+ if (parseTaskBranch(branch) === null) {
49
+ return {
50
+ verdict: 'allow',
51
+ reason: `Branch \`${branch}\` is not a task/<tranche>/<n> branch — the first-push dispatch gate only applies to task branches.`
52
+ }
53
+ }
54
+
55
+ if (prExists) {
56
+ return {
57
+ verdict: 'allow',
58
+ reason: `Branch \`${branch}\` already has an open PR — dispatch readiness was already validated on its first push; later pushes are not re-blocked by a sibling task's state change.`
59
+ }
60
+ }
61
+
62
+ if (readiness === 'UNKNOWN') {
63
+ return {
64
+ verdict: 'allow',
65
+ reason: `verify-dispatch could not reach the forge for \`${branch}\` (repo/token unresolvable) — failing OPEN rather than blocking the push on a transient issue.`
66
+ }
67
+ }
68
+
69
+ if (readiness === 'NOT_READY') {
70
+ return {
71
+ verdict: 'refuse',
72
+ reason: `verify-dispatch reports NOT READY for \`${branch}\` — see the failing predicate printed above.`
73
+ }
74
+ }
75
+
76
+ return {
77
+ verdict: 'allow',
78
+ reason: `verify-dispatch reports READY TO DISPATCH for \`${branch}\`.`
79
+ }
80
+ }