@attalabs/vinaya 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +13 -3
  2. package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
  3. package/aeg-root/contracts/brief-developer.md +1 -0
  4. package/aeg-root/contracts/developer-reviewer.md +3 -3
  5. package/aeg-root/contracts/reviewer-archivist.md +3 -3
  6. package/aeg-root/enforcement.md +26 -20
  7. package/aeg-root/process.md +8 -4
  8. package/aeg-root/roles/archivist.md +2 -2
  9. package/aeg-root/roles/brief-author.md +9 -3
  10. package/aeg-root/roles/developer.md +63 -31
  11. package/aeg-root/roles/planner.md +1 -1
  12. package/aeg-root/roles/principal.md +14 -0
  13. package/aeg-root/roles/reviewer.md +32 -15
  14. package/aeg-root/roles/security.md +15 -5
  15. package/aeg-root/skills/aeg/SKILL.md +9 -5
  16. package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
  17. package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
  18. package/aeg-root/state-machine.md +9 -11
  19. package/aeg-root/templates/brief-template.md +6 -6
  20. package/aeg-root/templates/pr-report-template.md +10 -7
  21. package/aeg-root/tranche-model.md +6 -2
  22. package/dist/checks/bin/check-body-bare-digits.js +552 -139
  23. package/dist/checks/bin/check-branch-topology.js +603 -144
  24. package/dist/checks/bin/check-brief-shape.js +593 -142
  25. package/dist/checks/bin/check-changeset-coverage.js +935 -161
  26. package/dist/checks/bin/check-closes-n.js +603 -144
  27. package/dist/checks/bin/check-coherence.js +614 -147
  28. package/dist/checks/bin/check-dead-branch-push.js +552 -139
  29. package/dist/checks/bin/check-dispatch-readiness.js +612 -145
  30. package/dist/checks/bin/check-doc-coverage-push.js +689 -218
  31. package/dist/checks/bin/check-doc-coverage.js +689 -218
  32. package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
  33. package/dist/checks/bin/check-doctrine-portability.js +684 -146
  34. package/dist/checks/bin/check-evidence-fresh.js +994 -174
  35. package/dist/checks/bin/check-exec-bits.js +4931 -0
  36. package/dist/checks/bin/check-first-push-dispatch.js +603 -144
  37. package/dist/checks/bin/check-issue-assignment.js +603 -144
  38. package/dist/checks/bin/check-main-branch-refusal.js +552 -139
  39. package/dist/checks/bin/check-no-disk-state.js +552 -139
  40. package/dist/checks/bin/check-pr-report-density.js +4461 -0
  41. package/dist/checks/bin/check-quoted-command.js +621 -155
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
  43. package/dist/checks/bin/check-registry-gates.js +595 -144
  44. package/dist/checks/bin/check-retired-vocabulary.js +617 -154
  45. package/dist/checks/bin/check-review-gate.js +670 -141
  46. package/dist/checks/bin/check-single-plan-pr.js +552 -139
  47. package/dist/checks/bin/check-test-plan.js +553 -140
  48. package/dist/checks/bin/check-token-collection-wired.js +552 -139
  49. package/dist/checks/bin/check-token-report.js +552 -139
  50. package/dist/checks/bin/check-workspace-escape.js +985 -145
  51. package/dist/index.js +8500 -5518
  52. package/package.json +1 -1
  53. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
  54. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
  55. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
  56. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
  57. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
  58. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
  59. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
  60. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
  61. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
  62. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
  63. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
  64. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
  65. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
  66. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
  67. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
  68. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
  69. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
  70. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
  71. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
  72. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
  73. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
  74. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
  75. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
  76. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
  77. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
  78. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
  79. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
  80. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
  81. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
  82. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
  83. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
  84. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
  85. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
  86. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
  87. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
  88. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
  89. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
  90. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
  91. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
  92. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
  93. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
  94. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
  95. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
  96. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
  97. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
  98. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
  99. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
  100. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
  101. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
  102. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
  103. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
  104. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
  105. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
  106. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
  107. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
  108. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
  109. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
  110. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
  111. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
  112. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
  113. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
  114. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
  115. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
  116. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
  117. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
  118. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
  119. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
  120. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
  121. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
  122. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
  123. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
  124. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  125. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  126. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  127. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  128. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  129. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  131. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  132. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  133. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  135. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  143. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  144. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  145. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  147. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  148. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  149. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  150. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  151. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  152. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  153. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  154. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  155. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  156. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  157. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  158. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  159. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  160. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  161. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  162. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  163. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
  164. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  165. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  166. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  167. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  168. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  169. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1mcitst._.js} +1 -1
  170. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  171. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  172. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  173. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  174. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
  175. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  176. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  177. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  178. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  179. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  180. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  181. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
  182. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
  183. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
  184. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  185. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  186. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
  187. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
  188. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * state-machine-model.ts — the canonical, code-owned model of AEG's execution
3
+ * state machine: what it reads, what it can conclude, and the ordered rules
4
+ * that get from one to the other. Pure data, zero I/O — the same discipline as
5
+ * `actions.ts` and `waiver-label.ts` (one pure-data list, read by both
6
+ * the logic and the rendered docs, so the two can never drift). Its only
7
+ * non-type import is `label()` from the sibling pure-data label vocabulary
8
+ * (`@attalabs/aeg-forge-state`'s `labels.ts`), so the label names this model quotes
9
+ * cannot drift from the ones the mechanism actually matches.
10
+ *
11
+ * Three parts, in the order a reader needs them:
12
+ *
13
+ * 1. `FORGE_FACT_INPUTS` — every `ForgeFacts` field and the GitHub object it
14
+ * is read from. The inputs are the whole source of truth: status is
15
+ * *derived* from forge objects, never written down. Descriptive
16
+ * only — the mapping itself is performed by `map-forge-facts.ts`.
17
+ * 2. `DERIVED_STATUSES` — the statuses derivation can conclude.
18
+ * 3. `DERIVATION_RULES` — the ordered rule list, first match wins.
19
+ *
20
+ * **Order is load-bearing, not incidental.** `blocked` must win over every
21
+ * other conclusion; the reopened-after-merge rules must precede the plain
22
+ * `merged` rule or a reopened Issue would permanently read `merged` off a
23
+ * stale fact. `deriveStatus` (`derive-tranche.ts`) executes this list
24
+ * verbatim, so reordering entries here changes real status everywhere — every
25
+ * gate, the Studio, and the CLI read their status through it.
26
+ *
27
+ * **Deliberately not imported here: the label vocabulary.** Derivation works
28
+ * on `ForgeFacts` — booleans and enums — never on label *strings*; the one
29
+ * label that matters arrives pre-read as the `blockedLabel` fact. Importing
30
+ * `@attalabs/aeg-forge-state` for its `LABELS` would also drag that package's
31
+ * `node:child_process` into any browser bundle rendering this model, the same
32
+ * hazard `diagram-model.ts` documents for `ACTIONS`. Label names live in
33
+ * `@attalabs/aeg-forge-state`'s `labels.ts`; this module names them only in prose.
34
+ */
35
+
36
+ import { label } from '@attalabs/aeg-forge-state'
37
+ import type { ForgeFacts } from '@attalabs/aeg-types'
38
+ import type { DerivedStatus } from './types'
39
+
40
+ // ---------- 1. Inputs ----------
41
+
42
+ export type ForgeFactInput = {
43
+ /** The `ForgeFacts` field, as the deriver sees it. */
44
+ fact: keyof ForgeFacts
45
+ /** The GitHub object and field it is read from, before mapping. */
46
+ readsFrom: string
47
+ /** What the fact means to derivation. */
48
+ meaning: string
49
+ }
50
+
51
+ /**
52
+ * One entry per `ForgeFacts` field. `closedAt` / `mergedAt` are carried for
53
+ * the coherence oracle rather than for status, and are marked as such — they
54
+ * are inputs to the model's world, but no derivation rule reads them.
55
+ */
56
+ export const FORGE_FACT_INPUTS: ForgeFactInput[] = [
57
+ {
58
+ fact: 'issueState',
59
+ readsFrom: 'Issue.state (OPEN | CLOSED)',
60
+ meaning: 'Whether the task Issue is still open — the anchor for the honest terminal statuses.'
61
+ },
62
+ {
63
+ fact: 'assigned',
64
+ readsFrom: 'Issue.assignees (count > 0)',
65
+ meaning: 'An assignee exists. No longer affects derivation: assigned and unassigned are both todo.'
66
+ },
67
+ {
68
+ fact: 'branchExists',
69
+ readsFrom: 'Ref refs/heads/task/<tranche>/<id>',
70
+ meaning: 'A task branch has been published — the todo → in-flight transition, written by git push, not by hand.'
71
+ },
72
+ {
73
+ fact: 'prState',
74
+ readsFrom: 'PullRequest.state (OPEN | CLOSED | MERGED)',
75
+ meaning: 'Whether work is proposed or landed. CLOSED-without-merge collapses to none — AEG models open/merged/none.'
76
+ },
77
+ {
78
+ fact: 'reviewDecision',
79
+ readsFrom: 'PullRequest.reviewDecision',
80
+ meaning: "Only 'changes_requested' flips status; approved-but-unmerged stays in-review."
81
+ },
82
+ {
83
+ fact: 'blockedLabel',
84
+ readsFrom: `Issue.labels contains '${label('blocked')}'`,
85
+ meaning: 'Execution halted pending an external unblock. Wins over every other rule.'
86
+ },
87
+ {
88
+ fact: 'stateReason',
89
+ readsFrom: 'Issue.stateReason (COMPLETED | NOT_PLANNED | REOPENED | null)',
90
+ meaning: 'Separates a legitimate drop from an incoherent close on a closed, never-merged Issue.'
91
+ },
92
+ {
93
+ fact: 'closedAt',
94
+ readsFrom: 'Issue.closedAt',
95
+ meaning: 'Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it.'
96
+ },
97
+ {
98
+ fact: 'mergedAt',
99
+ readsFrom: 'PullRequest.mergedAt',
100
+ meaning: 'Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it.'
101
+ },
102
+ {
103
+ fact: 'closedByActor',
104
+ readsFrom: 'ClosedEvent.actor.login (most recent timeline CLOSED_EVENT)',
105
+ meaning:
106
+ 'GitHub login that hand-closed the Issue. No task-status derivation rule reads it — it is consumed by dispatch-gate.ts/coherence-checks.ts A1 to recognize a hand-closed dependency as resolved (task vinaya-engine-v1 21, #99), not by this status model.'
107
+ }
108
+ ]
109
+
110
+ // ---------- 2. Statuses ----------
111
+
112
+ /**
113
+ * Every value `DerivedStatus` admits. `backlog` is a project-level concept and
114
+ * is never emitted by derivation inside a tranche — tranche tasks
115
+ * are committed work, so their floor is `todo`. It stays in the set because
116
+ * the type still admits it and consumers still render it.
117
+ */
118
+ export const DERIVED_STATUSES: DerivedStatus[] = [
119
+ 'backlog',
120
+ 'todo',
121
+ 'in-flight',
122
+ 'in-review',
123
+ 'changes-requested',
124
+ 'merged',
125
+ 'blocked',
126
+ 'dropped',
127
+ 'incoherent'
128
+ ]
129
+
130
+ /** The statuses derivation can actually conclude — every value except `backlog`. */
131
+ export const DERIVABLE_STATUSES: DerivedStatus[] = DERIVED_STATUSES.filter((s) => s !== 'backlog')
132
+
133
+ // ---------- 3. Rules ----------
134
+
135
+ export type DerivationRule = {
136
+ /** Stable identifier — safe to cite from docs and tests. */
137
+ id: string
138
+ /**
139
+ * Which step of the canonical chain this rule implements. Two steps branch
140
+ * on a second fact and are expressed as an adjacent pair of rules sharing a
141
+ * step number, so that every rule stays a flat predicate → status pair
142
+ * rather than a status-returning function.
143
+ */
144
+ chainStep: number
145
+ /** The condition in prose, for rendered docs. */
146
+ when: string
147
+ /** The condition in code. First rule to match decides the status. */
148
+ matches: (facts: ForgeFacts | undefined) => boolean
149
+ /** The status this rule concludes. */
150
+ status: DerivedStatus
151
+ /** Why the rule sits at this position — the part reordering would destroy. */
152
+ why: string
153
+ }
154
+
155
+ /**
156
+ * The ordered chain. First match wins; the final rule matches unconditionally,
157
+ * so the list is total and evaluation always concludes.
158
+ *
159
+ * Every predicate is written to be safe against `undefined` facts even though
160
+ * only the first rule can observe that case — the guard is structural, so
161
+ * adding a rule above the others can never turn into a null dereference.
162
+ */
163
+ export const DERIVATION_RULES: DerivationRule[] = [
164
+ {
165
+ id: 'no-facts',
166
+ chainStep: 1,
167
+ when: 'No forge facts for this task',
168
+ matches: (facts) => !facts,
169
+ status: 'todo',
170
+ why: 'A task absent from the forge snapshot is committed work not yet started — never backlog, inside a tranche.'
171
+ },
172
+ {
173
+ id: 'blocked-label',
174
+ chainStep: 2,
175
+ when: 'The blocked label is present',
176
+ matches: (facts) => facts?.blockedLabel === true,
177
+ status: 'blocked',
178
+ why: 'Blocked wins over every other conclusion — a halted task must not read as progressing, whatever its branch or PR says.'
179
+ },
180
+ {
181
+ id: 'reopened-after-merge-with-branch',
182
+ chainStep: 3,
183
+ when: 'Issue is open, a PR already merged, and a task branch exists',
184
+ matches: (facts) => facts?.issueState === 'open' && facts.prState === 'merged' && facts.branchExists,
185
+ status: 'in-flight',
186
+ why: 'Must precede the plain merged rule: the Issue reopened and new work is already published, so the historical merge is stale.'
187
+ },
188
+ {
189
+ id: 'reopened-after-merge-no-branch',
190
+ chainStep: 3,
191
+ when: 'Issue is open and a PR already merged, but no task branch exists',
192
+ matches: (facts) => facts?.issueState === 'open' && facts.prState === 'merged',
193
+ status: 'todo',
194
+ why: 'Same reopen case, work not restarted. Without it a reopened Issue would read merged forever — GitHub never retracts merge history.'
195
+ },
196
+ {
197
+ id: 'pr-merged',
198
+ chainStep: 4,
199
+ when: 'A PR for this task is merged',
200
+ matches: (facts) => facts?.prState === 'merged',
201
+ status: 'merged',
202
+ why: 'The terminal success state. Reached only once the reopen exceptions above have declined the facts.'
203
+ },
204
+ {
205
+ id: 'pr-open-changes-requested',
206
+ chainStep: 5,
207
+ when: 'A PR is open and review requested changes',
208
+ matches: (facts) => facts?.prState === 'open' && facts.reviewDecision === 'changes_requested',
209
+ status: 'changes-requested',
210
+ why: 'Must precede the plain open-PR rule, which would otherwise swallow it as in-review and hide that the ball is back with the Developer.'
211
+ },
212
+ {
213
+ id: 'pr-open',
214
+ chainStep: 5,
215
+ when: 'A PR is open',
216
+ matches: (facts) => facts?.prState === 'open',
217
+ status: 'in-review',
218
+ why: 'Opening the PR is itself the in-flight → in-review transition; no status is ever written to record it.'
219
+ },
220
+ {
221
+ id: 'branch-exists',
222
+ chainStep: 6,
223
+ when: 'A task branch exists, with no PR',
224
+ matches: (facts) => facts?.branchExists === true,
225
+ status: 'in-flight',
226
+ why: 'Publishing the branch is the todo → in-flight transition. Below the PR rules, since a PR is the stronger signal.'
227
+ },
228
+ {
229
+ id: 'issue-open',
230
+ chainStep: 7,
231
+ when: 'The Issue is open, with no branch and no PR',
232
+ matches: (facts) => facts?.issueState === 'open',
233
+ status: 'todo',
234
+ why: 'Not started. Assigned or not, both are todo inside a tranche.'
235
+ },
236
+ {
237
+ id: 'closed-not-planned',
238
+ chainStep: 8,
239
+ when: 'Issue closed NOT_PLANNED, with no merged PR',
240
+ matches: (facts) => facts?.stateReason === 'not_planned',
241
+ status: 'dropped',
242
+ why: 'A legitimately abandoned task. Reached only when the Issue is closed and nothing merged.'
243
+ },
244
+ {
245
+ id: 'closed-without-merge',
246
+ chainStep: 8,
247
+ when: 'Issue closed for any other reason, with no merged PR',
248
+ matches: () => true,
249
+ status: 'incoherent',
250
+ why: 'The honest terminal fallback: closed but unprovable must never read as todo. Total by design, so derivation always concludes.'
251
+ }
252
+ ]
253
+
254
+ /**
255
+ * Execute the model: the first rule whose predicate matches decides the
256
+ * status. `deriveStatus` in `derive-tranche.ts` delegates here, so the
257
+ * rendered rules and the real derivation are the same list — the whole point
258
+ * of the discipline.
259
+ *
260
+ * The trailing return is unreachable by construction (the final rule matches
261
+ * unconditionally) and exists only to satisfy the type checker;
262
+ * `state-machine-model.test.ts` asserts that totality directly.
263
+ */
264
+ export function deriveStatusFromModel(facts: ForgeFacts | undefined): DerivedStatus {
265
+ for (const rule of DERIVATION_RULES) {
266
+ if (rule.matches(facts)) return rule.status
267
+ }
268
+ return 'incoherent'
269
+ }
@@ -0,0 +1,14 @@
1
+ import type { Tranche } from './types'
2
+
3
+ /**
4
+ * The seam the pure evaluators (`deriveTranche`, `sumLedger`, ...) consume
5
+ * state through, instead of a raw `Tranche` tied to one storage shape.
6
+ * Implementations (forge-backed, file-backed) perform I/O and therefore live
7
+ * outside `aeg-core` (`apps/vinaya/sources`) — this package only defines the
8
+ * contract. Async on both adapters, even though the file adapter's own read
9
+ * is sync-capable, so callers get one uniform type regardless of which
10
+ * adapter is selected.
11
+ */
12
+ export type StateSource = {
13
+ getTranche(slug: string): Promise<Tranche>
14
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Spec status-block check (C1/F1). Pure — takes file
3
+ * content as a string, returns findings. No filesystem, no git.
4
+ */
5
+
6
+ export function hasStatusBlock(content: string): boolean {
7
+ return /(^|\n)\s*(\*\*)?Status:?(\*\*)?\s*:?\s*(draft|target|ratified|retired)/i.test(content)
8
+ }
@@ -0,0 +1,26 @@
1
+ import type { LedgerRow, LedgerTotals } from './types'
2
+
3
+ /**
4
+ * Pure aggregate over ledger rows. The tranche total is derived from the
5
+ * rows — never stored — so re-entry (a role appending a second row for the
6
+ * same phase) naturally adds to the total without any "replace existing"
7
+ * semantics. This is the same philosophy as derived task status and the
8
+ * append-only ledger: don't store the aggregate, sum the immutable
9
+ * entries.
10
+ *
11
+ * Null cells contribute zero — a claude.ai row the Principal has not yet
12
+ * filled in does not inflate the total with imaginary tokens. The row
13
+ * count includes those rows so a UI can still surface "5 entries, 2
14
+ * pending fill".
15
+ */
16
+ export function sumLedger(rows: LedgerRow[]): LedgerTotals {
17
+ let tokensIn = 0
18
+ let tokensOut = 0
19
+ let cost = 0
20
+ for (const r of rows) {
21
+ if (r.tokensIn != null) tokensIn += r.tokensIn
22
+ if (r.tokensOut != null) tokensOut += r.tokensOut
23
+ if (r.cost != null) cost += r.cost
24
+ }
25
+ return { tokensIn, tokensOut, cost, rows: rows.length }
26
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Two files in one package declaring the same symbol name.
3
+ *
4
+ * Not a style rule — a verification hazard. `parse-registry.ts` and
5
+ * `registry-parse.ts` are transpositions of each other, `parse-tranche.ts` is a
6
+ * third neighbour, and all three declare an unexported `stripBackticks`. So
7
+ * "the registry's backtick stripper" names nothing a reader can resolve, and
8
+ * checking a claim about it by grepping returns a confident answer about
9
+ * whichever copy the search landed on. That happened twice while writing
10
+ * atta-labs/vinaya#181: once in a verification pass that reached the right
11
+ * conclusion for the wrong reason, once in a filed Issue that sent a reader to
12
+ * the wrong file. `parse-registry.ts` was also binary to git at the time (see
13
+ * `no-binary-sources.test.ts`), so `git grep` could not have shown its copy.
14
+ *
15
+ * The lint does not forbid the collision. Plenty are harmless — a `makeTask`
16
+ * helper in two test files collides with nothing anyone reasons about. It
17
+ * reports them so a name that cannot be resolved by eye is known to be one.
18
+ *
19
+ * Deliberately syntactic. Resolving *references* needs the compiler API and is
20
+ * a different, larger job; this only reads declarations, so it is fast, has no
21
+ * dependencies, and cannot silently degrade the way a text search does.
22
+ */
23
+ export type SymbolDeclaration = { name: string; file: string; exported: boolean }
24
+ export type SymbolCollision = { name: string; files: string[]; anyExported: boolean }
25
+
26
+ /**
27
+ * A top-level declaration — no leading whitespace, so nested and block-scoped
28
+ * declarations are out of scope by construction.
29
+ *
30
+ * `const enum` is matched before bare `const`, or `export const enum E` would
31
+ * capture the keyword `enum` as the symbol name.
32
+ *
33
+ * KNOWN BLIND SPOTS, stated rather than papered over — a detector that implies
34
+ * coverage it does not have is the failure class it was built to catch:
35
+ * - a multi-declarator statement (`const a = 1, b = 2`) reports only the
36
+ * first name;
37
+ * - column-0 text inside a template literal or a block comment is read as
38
+ * code, so either can produce a false hit;
39
+ * - `export { x } from './y'` re-exports are not declarations and are not
40
+ * reported — correct for this purpose, but it means this does not measure
41
+ * a module's export surface.
42
+ * Closing these needs a real parse, which is the compiler API and a far larger
43
+ * job than this file.
44
+ */
45
+ const DECLARATION =
46
+ /^(export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?|class|interface|type|const\s+enum|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/gm
47
+
48
+ export function declarationsIn(file: string, source: string): SymbolDeclaration[] {
49
+ const out: SymbolDeclaration[] = []
50
+ DECLARATION.lastIndex = 0
51
+ let m: RegExpExecArray | null = DECLARATION.exec(source)
52
+ while (m !== null) {
53
+ out.push({ name: m[2] as string, file, exported: m[1] !== undefined })
54
+ m = DECLARATION.exec(source)
55
+ }
56
+ return out
57
+ }
58
+
59
+ /**
60
+ * Names declared in more than one file. `files` is sorted so the report is
61
+ * stable across filesystem ordering — a lint that reorders between runs
62
+ * produces diff noise and gets ignored, which is how a lint stops working.
63
+ */
64
+ export function findCollisions(decls: SymbolDeclaration[]): SymbolCollision[] {
65
+ const byName = new Map<string, SymbolDeclaration[]>()
66
+ for (const d of decls) {
67
+ const list = byName.get(d.name) ?? []
68
+ list.push(d)
69
+ byName.set(d.name, list)
70
+ }
71
+ const out: SymbolCollision[] = []
72
+ for (const [name, list] of byName) {
73
+ const files = [...new Set(list.map((d) => d.file))].sort()
74
+ if (files.length < 2) continue
75
+ out.push({ name, files, anyExported: list.some((d) => d.exported) })
76
+ }
77
+ return out.sort((a, b) => a.name.localeCompare(b.name))
78
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Test Plan checkbox-state gate evaluator (aeg-governance-hardening
3
+ * task 25, #365). Pure — no `fs`, no `gh`/`git`. The CLI shim
4
+ * (`bin/verify-test-plan.ts`) reads `PR_BODY`/`BRANCH` from env and prints
5
+ * this function's messages.
6
+ *
7
+ * See `locateTestPlanSection` for section-boundary detection (both the
8
+ * inline `**Test Plan:**` marker and the `## N. Test Plan` heading form).
9
+ * This module owns the checkbox tick-state decision built on top of it:
10
+ *
11
+ * - No section found, task branch → `fail` (a task PR without a
12
+ * Test Plan is malformed, not exempt — the exact #377 live-fire gap).
13
+ * - No section found, non-task branch (or no BRANCH) → `pass` (advisory).
14
+ * - `Test Plan: unit-tests-only` sentinel → `pass`.
15
+ * - Any unticked `- [ ]` checkbox → `fail`, naming every unticked line.
16
+ * - All boxes ticked (or no checkbox items at all) → `pass`.
17
+ */
18
+
19
+ import { locateTestPlanSection } from './test-plan-section'
20
+
21
+ export type TestPlanGateVerdict = 'pass' | 'fail'
22
+ export type TestPlanGateResult = { verdict: TestPlanGateVerdict; messages: string[] }
23
+
24
+ const TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/
25
+
26
+ export function evaluateTestPlanGate(body: string, branch: string): TestPlanGateResult {
27
+ if (!body) {
28
+ return {
29
+ verdict: 'pass',
30
+ messages: [
31
+ 'PR_BODY env var is empty; nothing to check.',
32
+ 'PASS (no body — likely a local invocation; CI sets PR_BODY automatically).'
33
+ ]
34
+ }
35
+ }
36
+
37
+ const located = locateTestPlanSection(body)
38
+
39
+ if (!located.found) {
40
+ if (TASK_BRANCH_PATTERN.test(branch)) {
41
+ return {
42
+ verdict: 'fail',
43
+ messages: [
44
+ `FAIL — no Test Plan section found in the PR body for task branch \`${branch}\`.`,
45
+ 'Searched for a `## Test Plan` / `## N. Test Plan` heading and an inline `**Test Plan:**`/`Test Plan:` marker — neither was found.',
46
+ 'Per, a task PR without a Test Plan is malformed, not exempt — add one (or `Test Plan: unit-tests-only` for a pure-logic brief with no runtime surface).'
47
+ ]
48
+ }
49
+ }
50
+ return {
51
+ verdict: 'pass',
52
+ messages: [
53
+ 'No Test Plan section in PR body.',
54
+ 'PASS (advisory) — see aeg-root/roles/developer.md (Verification) and aeg-root/skills/brief-authoring/SKILL.md §9.',
55
+ 'Non-task branch (or BRANCH unset) — a brief that touches a runtime surface and has no Test Plan is malformed on a task branch; Brief Validation catches that case pre-dispatch.'
56
+ ]
57
+ }
58
+ }
59
+
60
+ const section = located.section
61
+
62
+ if (/unit-tests-only/i.test(section)) {
63
+ return {
64
+ verdict: 'pass',
65
+ messages: ['Test Plan: unit-tests-only — pure-logic brief, CI unit-tests are the proof.', 'PASS.']
66
+ }
67
+ }
68
+
69
+ const checkboxLines = section
70
+ .split('\n')
71
+ .map((line) => line.trimStart())
72
+ .filter((line) => /^[-*]\s+\[[ xX]\]/.test(line))
73
+
74
+ if (checkboxLines.length === 0) {
75
+ return {
76
+ verdict: 'pass',
77
+ messages: [
78
+ 'Test Plan section found but has no checkbox items.',
79
+ "PASS (advisory) — the section may be empty or expressed in prose; verification is the Principal's call."
80
+ ]
81
+ }
82
+ }
83
+
84
+ const unchecked = checkboxLines.filter((line) => /^[-*]\s+\[\s\]/.test(line))
85
+ const checked = checkboxLines.filter((line) => /^[-*]\s+\[[xX]\]/.test(line))
86
+
87
+ const messages = [`Test Plan items: ${checked.length} ticked, ${unchecked.length} unticked.`]
88
+
89
+ if (unchecked.length > 0) {
90
+ messages.push('', 'FAIL — the following Test Plan items are unticked:')
91
+ for (const line of unchecked) messages.push(` ${line}`)
92
+ messages.push(
93
+ '',
94
+ 'Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any Test Plan box is unticked.',
95
+ '- [agent] items: the Developer-agent posts the actual command output as evidence and ticks the box.',
96
+ '- [principal] items: the Principal runs the item in a real signed-in browser and ticks the box.',
97
+ 'Note: editing the PR body does not retrigger most workflows. If your PR body changes do not surface here, push an empty commit to re-run.'
98
+ )
99
+ return { verdict: 'fail', messages }
100
+ }
101
+
102
+ messages.push('All Test Plan items ticked — runtime verification complete.', 'PASS.')
103
+ return { verdict: 'pass', messages }
104
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Test Plan section locator (aeg-governance-hardening task 25, #365). Pure —
3
+ * no `fs`, no `gh`/`git`. Extracted from `bin/verify-test-plan.ts`'s original
4
+ * inline-only regex to fix the live-fire gap PR #377 exposed: its body used
5
+ * the `## 9. Test Plan` heading form, which the old inline-only regex never
6
+ * matched, so `verify-test-plan` reported "no section" and advisory-PASSED a
7
+ * PR that actually had an unticked `**[principal]**` box.
8
+ *
9
+ * Recognizes BOTH forms `roles/developer.md`'s canonical PR-body template and
10
+ * real brief numbering produce:
11
+ * - Heading form: `## Test Plan`, `## 9. Test Plan`, `### 9a. Test Plan`
12
+ * - Inline form: `**Test Plan:**`, `Test Plan:` (optionally followed on
13
+ * the same line by the `unit-tests-only` sentinel)
14
+ *
15
+ * NOT unified with `brief-validation.ts`'s `checkTestPlan`/
16
+ * `checkTestPlanExclusivity` (the closest analogs to the brief's named
17
+ * `checkPlanShape`, which does not exist under that name in this codebase):
18
+ * those are whole-body presence checks (a Test Plan section is not
19
+ * even located — they match `**[agent]**`/`**[principal]**` tags or the
20
+ * `unit-tests-only` sentinel anywhere in the PR body), used pre-dispatch to
21
+ * validate brief shape. This module locates section BOUNDARIES to check
22
+ * checkbox tick-state at PR-open/push time — a different property. Unifying
23
+ * would force `checkTestPlan` to become section-scoped, changing its
24
+ * already-shipped pass/fail behavior — see this task's PR body for the
25
+ * decision record.
26
+ */
27
+
28
+ import { anchoredRegion } from './anchored-region'
29
+
30
+ export type TestPlanSection = { found: true; section: string } | { found: false }
31
+
32
+ const HEADING_FORM_RE = /^#{1,4}[ \t]*(?:\d+[a-z]?\.[ \t]*)?\*{0,2}Test Plan\*{0,2}[ \t]*$/im
33
+ const INLINE_FORM_RE = /^[ \t]*\*{0,2}Test Plan:?\*{0,2}/im
34
+ const NEXT_SECTION_RE = /\n(?:#{1,3}[ \t]|\*\*[A-Z][^*\n]*:\*\*)/
35
+
36
+ /**
37
+ * Locates the Test Plan section (heading form preferred, falling back to the
38
+ * inline marker form) and slices it out up to — but not including — the next
39
+ * heading or `**Field:**`-style section boundary. Returns `{ found: false }`
40
+ * when neither form is present anywhere in the body.
41
+ *
42
+ * When the body carries an `AEG:TEST-PLAN` anchor pair (`anchored-region.ts`,
43
+ * task 30), the pair IS the section: its whole content is returned verbatim,
44
+ * with no heading/inline marker required inside it — requiring a second,
45
+ * redundant in-region marker would reintroduce the unrecognized-heading
46
+ * failure class (#377) the anchors exist to close. Identical-looking Test
47
+ * Plan text elsewhere in the body (a pasted reference brief's own §9, the
48
+ * PR #392 shape) is ignored. Bodies without the pair parse exactly as before.
49
+ */
50
+ export function locateTestPlanSection(body: string): TestPlanSection {
51
+ const region = anchoredRegion(body, 'TEST-PLAN')
52
+ if (region !== null) return { found: true, section: region }
53
+
54
+ const headingMatch = HEADING_FORM_RE.exec(body)
55
+ const match = headingMatch ?? INLINE_FORM_RE.exec(body)
56
+ if (!match || match.index === undefined) return { found: false }
57
+
58
+ const sectionBody = body.slice(match.index)
59
+ const firstLineEnd = sectionBody.indexOf('\n')
60
+ const searchFrom = firstLineEnd === -1 ? sectionBody.length : firstLineEnd + 1
61
+ const nextMatch = sectionBody.slice(searchFrom).match(NEXT_SECTION_RE)
62
+ const sectionEnd = nextMatch ? searchFrom + (nextMatch.index ?? 0) : sectionBody.length
63
+
64
+ return { found: true, section: sectionBody.slice(0, sectionEnd) }
65
+ }