@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,306 @@
1
+ /**
2
+ * C7 — the published-prose check. Pure and zero-I/O, shaped like
3
+ * `docs-coherence.ts` (parsed entries plus the model-backed surfaced-path
4
+ * allowlist in, `{ errors, notes }` out).
5
+ *
6
+ * A role or contract doc is read by two audiences with opposite needs: an
7
+ * agent taking the role reads it end to end as its operating instruction, and
8
+ * a person landing on `/docs/roles/<id>` reads the same file cold. The answer
9
+ * is one short, binding section at the top of every role and contract — `##
10
+ * The short version` — which is what publishes; everything under `##
11
+ * Reference` explains it and stays in the repo. This check enforces two
12
+ * properties of that section, over the published set only:
13
+ *
14
+ * 1. **Structure** — every surfaced `roles/*.md` and `contracts/*.md` opens
15
+ * with a `## The short version` section carrying all four bold-led
16
+ * blocks, in order, inside the word band. The blocks answer the same four
17
+ * questions for a role and for a seam (what it owns/carries, when it
18
+ * refuses, what it never does, how it physically runs), which is why the
19
+ * lead-ins differ by kind but the count and order never do.
20
+ * 2. **Readability** — the text that actually reaches a reader carries no
21
+ * token a stranger cannot resolve from the page: no decision id, no
22
+ * section sign, no forge number, no retired public name, no tranche
23
+ * slug, no repo-internal path, and no label vocabulary (namespaced or
24
+ * retired alike — a published page should name neither, and flagging only
25
+ * the current grammar would leave every retired mention invisible).
26
+ *
27
+ * **What "published" means here is deliberately narrow.** For a role or
28
+ * contract it is the short version, not the file. For `enforcement.md` it is
29
+ * the introduction plus the four columns the page renders — the row's own
30
+ * name, `Summary`, `Category`, `Description`. The enforcing columns (`Gate`,
31
+ * `What must be true…`, `Re-verifies`, `Runs`, `Catches`, `implementation`,
32
+ * `lock`) are never scanned: they never reach a page, and flagging them would
33
+ * demand rewriting the gate registry to suit a website. Columns resolve by
34
+ * HEADER NAME, never by index — the three ring tables have different shapes,
35
+ * and `Summary`/`Category`/`Description` are the ones spelled identically in
36
+ * all three, the same property `registry-parse.ts` relies on for
37
+ * `Description`.
38
+ *
39
+ * **Protocol mechanics are published on purpose.** `.worktrees/task/<tranche>/<n>`
40
+ * and the `task/<tranche>/<n>` branch convention are the method an adopter
41
+ * must learn, not this repo's internals — they are masked out before scanning
42
+ * so a path rule can never eat them, and that exemption is tested rather than
43
+ * incidental.
44
+ */
45
+
46
+ import { findHeadingLine, findTable } from '../markdown-table'
47
+ import { isSurfacedDoc } from './surfaced-manifest'
48
+ import type { DocFrontmatter } from './types'
49
+
50
+ export const SHORT_VERSION_HEADING = 'The short version'
51
+ export const REFERENCE_HEADING = 'Reference'
52
+
53
+ /** Words. Under the floor means something was dropped; over the ceiling means
54
+ * the reference is being summarised instead of the rule being stated. */
55
+ export const SHORT_VERSION_MIN_WORDS = 150
56
+ export const SHORT_VERSION_MAX_WORDS = 450
57
+
58
+ /** The four blocks, in order, as a role answers them. */
59
+ export const ROLE_BLOCKS = ['You own', 'You refuse', 'You never', 'How it physically runs'] as const
60
+
61
+ /** The same four blocks, as a seam answers them. */
62
+ export const CONTRACT_BLOCKS = [
63
+ 'What crosses',
64
+ 'The hand-off is malformed',
65
+ 'What it does not carry',
66
+ 'How it physically runs'
67
+ ] as const
68
+
69
+ /**
70
+ * Protocol facts that must survive every readability rule. Masked out of the
71
+ * text before scanning, longest first, so a repo-internal-path rule can never
72
+ * claim the branch convention.
73
+ */
74
+ export const ALLOWED_MECHANICS: readonly string[] = [
75
+ '.worktrees/task/<tranche>/<n>',
76
+ 'task/<tranche>/<n>',
77
+ '.worktrees/'
78
+ ]
79
+
80
+ export type PublishedProseEntry = {
81
+ /** Path relative to `aeg-root/`, e.g. `roles/developer.md`. */
82
+ relPath: string
83
+ frontmatter: Pick<DocFrontmatter, 'surfaced' | 'title' | 'description'>
84
+ body: string
85
+ }
86
+
87
+ export type PublishedProseResult = { errors: string[]; notes: string[] }
88
+
89
+ type ProseRule = { id: string; what: string; pattern: RegExp }
90
+
91
+ /**
92
+ * Every rule is case-sensitive on purpose where the token itself is: forge
93
+ * labels are lower-case, so a lower-case-only match keeps a capitalized
94
+ * English word ("Project:", the PR-body field) from being read as the deleted
95
+ * `project:*` label family.
96
+ */
97
+ const PROSE_RULES: ProseRule[] = [
98
+ { id: 'decision-id', what: 'a decision id', pattern: /\bD-\d{3,}\b/ },
99
+ { id: 'section-ref', what: 'a section reference', pattern: /§/ },
100
+ { id: 'forge-number', what: 'a forge number', pattern: /#\d+/ },
101
+ { id: 'retired-name', what: 'the retired public name', pattern: /\bAEG\b/ },
102
+ { id: 'tranche-slug', what: 'a tranche slug', pattern: /\b[a-z][a-z0-9]*(?:-[a-z0-9]+)+-v\d+\b/ },
103
+ {
104
+ id: 'label-vocabulary',
105
+ what: 'forge label vocabulary',
106
+ // Every entry here is CUMULATIVE — a retired namespace stays banned after
107
+ // the objects carrying it are gone, because a published page naming it is
108
+ // just as unreadable to a stranger as one naming the current grammar (see
109
+ // the module header). `aeg:` is retired and still listed for that reason,
110
+ // and `iteration:` joins it: the rename to `tranche:` ADDS an alternative,
111
+ // it does not substitute one. Substituting is how a rename silently
112
+ // narrows this gate — the forge still carried `vinaya/iteration:*` labels
113
+ // when the rename landed, and a page naming one would have passed.
114
+ pattern: /(?:vinaya\/)?\b(?:tranche|iteration|tier|needs|waiver|project|aeg):(?![/\s]|$)/
115
+ },
116
+ { id: 'internal-path', what: 'a repo-internal path', pattern: /\b(?:packages|apps|aeg-root|tools|scripts)\// },
117
+ { id: 'internal-path', what: 'a repo-internal path', pattern: /\.(?:claude|husky|github)\// },
118
+ {
119
+ id: 'internal-path',
120
+ what: 'a repo-internal file',
121
+ pattern: /(?:^|[\s(`"'[])[\w.-]+(?:\/[\w.-]+)*\.(?:ts|tsx|js|jsx|mjs|cjs|sh|ya?ml|json|md)\b/
122
+ }
123
+ ]
124
+
125
+ /** The doc kinds this check governs, and the block set each answers with. */
126
+ function blocksFor(relPath: string): readonly string[] | null {
127
+ if (relPath.startsWith('roles/')) return ROLE_BLOCKS
128
+ if (relPath.startsWith('contracts/')) return CONTRACT_BLOCKS
129
+ return null
130
+ }
131
+
132
+ function sectionBody(body: string, heading: string): string | null {
133
+ const lines = body.split('\n')
134
+ const start = lines.findIndex((line) => line.trim() === `## ${heading}`)
135
+ if (start === -1) return null
136
+ const rest = lines.slice(start + 1)
137
+ const end = rest.findIndex((line) => /^##\s/.test(line))
138
+ return (end === -1 ? rest : rest.slice(0, end)).join('\n').trim()
139
+ }
140
+
141
+ /**
142
+ * The section a role/contract page publishes, or null when the doc carries
143
+ * none. The single extraction both the gate and Vinaya's `/docs` renderer
144
+ * read, so the page and the check can never disagree about what "published"
145
+ * means.
146
+ */
147
+ export function extractShortVersion(body: string): string | null {
148
+ return sectionBody(body, SHORT_VERSION_HEADING)
149
+ }
150
+
151
+ /**
152
+ * What a role/contract page renders, with a degradation ladder that never
153
+ * leaks the reference: the short version when present; otherwise the body
154
+ * above `## Reference`; otherwise the whole body (a doc with neither heading
155
+ * is not governed by C7's structure rule and has no reference to hide).
156
+ */
157
+ export function publishedDoctrineBody(body: string): string {
158
+ const short = extractShortVersion(body)
159
+ if (short !== null) return short
160
+ const lines = body.split('\n')
161
+ const referenceAt = lines.findIndex((line) => line.trim() === `## ${REFERENCE_HEADING}`)
162
+ return referenceAt === -1 ? body : lines.slice(0, referenceAt).join('\n').trim()
163
+ }
164
+
165
+ export function countWords(text: string): number {
166
+ return text.split(/\s+/).filter(Boolean).length
167
+ }
168
+
169
+ function maskAllowedMechanics(text: string): string {
170
+ let masked = text
171
+ for (const literal of ALLOWED_MECHANICS) {
172
+ masked = masked.split(literal).join(' ')
173
+ }
174
+ return masked
175
+ }
176
+
177
+ /**
178
+ * The four columns `enforcement.md` actually renders — the row's own name plus
179
+ * `Summary`, `Category`, `Description` — and the introduction above the first
180
+ * ring. Everything else in those tables is enforcing text that never reaches a
181
+ * page.
182
+ */
183
+ export function enforcementPublishedText(content: string): string[] {
184
+ const lines = content.split('\n')
185
+ const out: string[] = []
186
+
187
+ const firstRing = lines.findIndex((line) => /^##\s+Ring 0\b/.test(line))
188
+ out.push((firstRing === -1 ? lines : lines.slice(0, firstRing)).join('\n'))
189
+
190
+ const publishedHeaders = ['summary', 'category', 'description']
191
+ for (const ring of [0, 1, 2]) {
192
+ const headingLine = findHeadingLine(lines, new RegExp(`^##\\s+Ring ${ring}\\b`))
193
+ if (headingLine === null) continue
194
+ const table = findTable(lines, headingLine + 1)
195
+ if (!table) continue
196
+ const indices = publishedHeaders
197
+ .map((name) => table.headers.findIndex((h) => h.trim().toLowerCase() === name))
198
+ .filter((i) => i !== -1)
199
+ for (const row of table.rows) {
200
+ out.push(row.cells[0] ?? '')
201
+ for (const i of indices) out.push(row.cells[i] ?? '')
202
+ }
203
+ }
204
+
205
+ return out.filter((text) => text.trim().length > 0)
206
+ }
207
+
208
+ /**
209
+ * The readability rules, callable on any published text — not just a doc body.
210
+ *
211
+ * Exported because `/docs/actions` renders prose that lives in TypeScript
212
+ * (`ACTIONS` in `actions.ts`), not in a markdown file, so `evaluatePublishedProse`
213
+ * never sees it. A whole published page was unguarded on that technicality;
214
+ * `actions.test.ts` closes it by running this over every entry.
215
+ */
216
+ export function readabilityErrors(where: string, texts: string[]): string[] {
217
+ const errors: string[] = []
218
+ checkReadability(where, texts, errors)
219
+ return errors
220
+ }
221
+
222
+ function checkReadability(where: string, texts: string[], errors: string[]): void {
223
+ const seen = new Set<string>()
224
+ for (const raw of texts) {
225
+ const text = maskAllowedMechanics(raw)
226
+ for (const rule of PROSE_RULES) {
227
+ const match = text.match(rule.pattern)
228
+ if (!match) continue
229
+ const token = (match[0] ?? '').trim()
230
+ const key = `${rule.id}:${token}`
231
+ if (seen.has(key)) continue
232
+ seen.add(key)
233
+ errors.push(`C7: published text in "${where}" contains ${rule.what} a reader cannot resolve: "${token}"`)
234
+ }
235
+ }
236
+ }
237
+
238
+ function checkStructure(relPath: string, body: string, blocks: readonly string[], errors: string[]): void {
239
+ const short = extractShortVersion(body)
240
+ if (short === null) {
241
+ errors.push(
242
+ `C7: surfaced doc "${relPath}" has no "## ${SHORT_VERSION_HEADING}" section — the published page would have nothing binding to show a reader`
243
+ )
244
+ return
245
+ }
246
+
247
+ let cursor = 0
248
+ for (const block of blocks) {
249
+ const at = short.indexOf(`**${block}`, cursor)
250
+ if (at === -1) {
251
+ errors.push(`C7: "${relPath}" short version is missing the "${block}" block (all four blocks always appear)`)
252
+ continue
253
+ }
254
+ cursor = at + block.length
255
+ }
256
+
257
+ const words = countWords(short)
258
+ if (words < SHORT_VERSION_MIN_WORDS) {
259
+ errors.push(
260
+ `C7: "${relPath}" short version is ${words} words, under the ${SHORT_VERSION_MIN_WORDS}-word floor — something was dropped`
261
+ )
262
+ } else if (words > SHORT_VERSION_MAX_WORDS) {
263
+ errors.push(
264
+ `C7: "${relPath}" short version is ${words} words, over the ${SHORT_VERSION_MAX_WORDS}-word ceiling — state the rule, don't summarise the reference`
265
+ )
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Runs both checks over the published set only. `entries` may hold every doc
271
+ * under `aeg-root/`; `surfacedPaths` (the model-backed allowlist) is what
272
+ * narrows it, so this check can never publish-check a doc the site does not
273
+ * publish, nor miss one it does.
274
+ */
275
+ export function evaluatePublishedProse(
276
+ entries: PublishedProseEntry[],
277
+ surfacedPaths: ReadonlySet<string> = new Set()
278
+ ): PublishedProseResult {
279
+ const errors: string[] = []
280
+ const notes: string[] = []
281
+
282
+ const surfaced = entries.filter((e) => isSurfacedDoc(e.relPath, e.frontmatter, surfacedPaths))
283
+
284
+ for (const entry of surfaced) {
285
+ // Frontmatter renders too: `title` is the page heading and the sidebar
286
+ // entry, `description` is the page metadata. Checking only the body left
287
+ // both unguarded — a citation in a title reaches a reader exactly as
288
+ // surely as one in a paragraph.
289
+ checkReadability(entry.relPath, [entry.frontmatter.title ?? '', entry.frontmatter.description ?? ''], errors)
290
+
291
+ const blocks = blocksFor(entry.relPath)
292
+ if (blocks) {
293
+ checkStructure(entry.relPath, entry.body, blocks, errors)
294
+ const short = extractShortVersion(entry.body)
295
+ if (short !== null) checkReadability(entry.relPath, [short], errors)
296
+ continue
297
+ }
298
+ if (entry.relPath === 'enforcement.md') {
299
+ checkReadability(entry.relPath, enforcementPublishedText(entry.body), errors)
300
+ }
301
+ }
302
+
303
+ if (surfaced.length === 0) notes.push('C7: no surfaced docs to check.')
304
+
305
+ return { errors, notes }
306
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The canonical "surfaced doc" manifest for `aeg-root/`. Defines, as
3
+ * data, which docs a public AEG page shows. The rule is **model-backed**: a
4
+ * doc is surfaced if and only if a `DiagramModel` node points at it.
5
+ * The same `DiagramModel` that `/docs/harness` renders is the allowlist for
6
+ * `/docs/reference` — the two surfaces are two renderers of one model, and the doc that
7
+ * backs no reachable node stops being public, every build.
8
+ *
9
+ * The node→doc mapping (`modelBackedDocPaths`) is the same one `read-more.ts`
10
+ * uses to resolve a node's "Read more" target: gate/check nodes back
11
+ * `enforcement.md`; a role node backs `roles/<id>.md`; a contract node backs
12
+ * `contracts/<id>.md`. `action`/`ring` nodes back no `aeg-root/**` document
13
+ * (an action's source is `packages/aeg-core/src/actions.ts`, a ring is a
14
+ * summary label), so they add nothing to the set. Today that is 16 files —
15
+ * `enforcement.md` + the 9 `roles/*.md` + the 6 `contracts/*.md`.
16
+ *
17
+ * This is the single source of truth the C6 docs-coherence check
18
+ * (`docs-coherence.ts`) and Vinaya's `/docs` loader both consume. There is
19
+ * **no second, path-based exclusion rule** — a competing rule is the failure
20
+ * mode this manifest exists to prevent, and the model-backed set replaced the
21
+ * old path-exclusion rules outright rather than sitting beside them. The only
22
+ * escape hatch is the per-doc `surfaced` frontmatter override, which wins in
23
+ * both directions.
24
+ *
25
+ * Purity: this module imports only the `DiagramModel` **type** from the
26
+ * diagram layer — no runtime coupling, no I/O. The doctrine is read (and the
27
+ * model derived) by the caller, which passes the derived path set in; aeg-core
28
+ * stays zero-I/O (#372/#382/#506).
29
+ *
30
+ * Paths are relative to `aeg-root/` (e.g. `roles/developer.md`), matching
31
+ * `DocFrontmatter`'s existing convention.
32
+ */
33
+
34
+ import type { DiagramModel } from '../diagram-model'
35
+ import type { DocFrontmatter } from './types'
36
+
37
+ /**
38
+ * The set of `aeg-root/`-relative doc paths a `DiagramModel` points at — the
39
+ * allowlist. Mirrors `read-more.ts`'s `docRoute` resolution exactly, so
40
+ * `/docs/reference`'s surfaced set and `/docs/harness`'s "Read more" targets can never
41
+ * name different files: gate/check → `enforcement.md`, role → `roles/<id>.md`,
42
+ * contract → `contracts/<id>.md`. A node's `label` is its doctrine id
43
+ * (`role.roleId` / `contract.contractId`), and every `roles/*.md` /
44
+ * `contracts/*.md` file is named `<id>.md` — one convention backs both the
45
+ * GitHub path and the docs route.
46
+ */
47
+ export function modelBackedDocPaths(model: DiagramModel): Set<string> {
48
+ const paths = new Set<string>()
49
+ for (const node of model.nodes) {
50
+ if (node.kind === 'gate' || node.kind === 'check') paths.add('enforcement.md')
51
+ else if (node.kind === 'role') paths.add(`roles/${node.label}.md`)
52
+ else if (node.kind === 'contract') paths.add(`contracts/${node.label}.md`)
53
+ }
54
+ return paths
55
+ }
56
+
57
+ const NO_MODEL_BACKED_PATHS: ReadonlySet<string> = new Set()
58
+
59
+ /**
60
+ * Whether `relPath` (relative to `aeg-root/`) is a surfaced doc. A boolean
61
+ * `surfaced` frontmatter field always wins over the model — the escape hatch
62
+ * in both directions. Otherwise the doc is surfaced iff it is in
63
+ * `surfacedPaths` (the model-backed allowlist from `modelBackedDocPaths`).
64
+ *
65
+ * `surfacedPaths` is optional so a caller that has no model surfaces nothing
66
+ * by default rather than everything — the safe direction for a rule whose
67
+ * whole point is that a doc publishes only when a node points at it.
68
+ */
69
+ export function isSurfacedDoc(
70
+ relPath: string,
71
+ frontmatter: Pick<DocFrontmatter, 'surfaced'>,
72
+ surfacedPaths: ReadonlySet<string> = NO_MODEL_BACKED_PATHS
73
+ ): boolean {
74
+ if (typeof frontmatter.surfaced === 'boolean') return frontmatter.surfaced
75
+ return surfacedPaths.has(relPath)
76
+ }
77
+
78
+ export type SurfacedManifestEntry = {
79
+ relPath: string
80
+ frontmatter: Pick<DocFrontmatter, 'surfaced'>
81
+ }
82
+
83
+ /** Filters a list of parsed doc entries down to the surfaced subset. */
84
+ export function surfacedDocs(
85
+ entries: SurfacedManifestEntry[],
86
+ surfacedPaths: ReadonlySet<string> = NO_MODEL_BACKED_PATHS
87
+ ): string[] {
88
+ return entries.filter((e) => isSurfacedDoc(e.relPath, e.frontmatter, surfacedPaths)).map((e) => e.relPath)
89
+ }
@@ -0,0 +1,33 @@
1
+ export type DocFrontmatter = {
2
+ title?: string
3
+ sidebarTitle?: string
4
+ description?: string
5
+ section?: string
6
+ order?: number
7
+ parent?: string
8
+ surfaced?: boolean
9
+ }
10
+
11
+ export type Doc = {
12
+ slug: string
13
+ title: string
14
+ sidebarTitle?: string
15
+ description?: string
16
+ section: string
17
+ order: number
18
+ href: string
19
+ filePath: string
20
+ children?: Doc[]
21
+ parentSlug?: string
22
+ }
23
+
24
+ export type DocSection = {
25
+ id: string
26
+ label: string
27
+ docs: Doc[]
28
+ }
29
+
30
+ export type DocNav = {
31
+ sections: DocSection[]
32
+ flat: Doc[]
33
+ }
@@ -0,0 +1,170 @@
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
+ * Zero I/O: every input (file paths + contents) is read by the adapter and
21
+ * passed in.
22
+ */
23
+
24
+ export type PortabilitySourceFile = { path: string; content: string }
25
+
26
+ export type PortabilityFinding = {
27
+ file: string
28
+ line: number
29
+ cited: string
30
+ message: string
31
+ }
32
+
33
+ /** `aeg-root/**` by default — what this repo's own package ships and every adopter installs read-only. */
34
+ const DEFAULT_SHIPS_PREFIX = 'aeg-root/'
35
+
36
+ /**
37
+ * Doctrine-relative (`roles/`, `contracts/`, `skills/`) and adopter-owned
38
+ * (`.github/`, `.vinaya/`, `.claude/`) top segments — the two portable
39
+ * classes measured in the task-234 corpus scan. `aeg-root/` itself is
40
+ * deliberately NOT a static entry here: it is always the CALLER-supplied
41
+ * `shipsPrefix`, checked dynamically in `isPortable` below, so an adopter
42
+ * who configures a non-default `doctrineRoot` gets that root treated as
43
+ * portable too — a static `'aeg-root/'` entry would falsely flag every one
44
+ * of that adopter's own self-citations as non-portable. A citation whose
45
+ * top segment falls outside this list (and isn't the ships prefix) is
46
+ * never assumed portable, however plausible it looks; it is a finding, and
47
+ * someone extends this list deliberately once its side of the
48
+ * classification is decided.
49
+ */
50
+ const STATIC_PORTABLE_PREFIXES: readonly string[] = [
51
+ 'roles/',
52
+ 'contracts/',
53
+ 'skills/',
54
+ '.github/',
55
+ '.vinaya/',
56
+ '.claude/',
57
+ // Not in the task-234 corpus-scan table, but verified while reading the
58
+ // corpus: `.git/hooks/*` and `.husky/*` are cited only as the pre-commit/
59
+ // pre-push hook locations every git repo (and every husky-adopting repo)
60
+ // genuinely has — adopter-owned by construction, the same class as
61
+ // `.github/`/`.vinaya/`/`.claude/` above.
62
+ '.git/',
63
+ '.husky/'
64
+ ]
65
+
66
+ /**
67
+ * The three illustrative placeholders measured in the corpus — a
68
+ * `[path/inside/the/...]`-shaped fill-in-the-blank in a template, never a
69
+ * real citation. Exempted by literal, not by pattern, so a real path that
70
+ * happens to share a prefix with one of these is never accidentally waved
71
+ * through.
72
+ */
73
+ const EXEMPT_LITERALS: ReadonlySet<string> = new Set([
74
+ 'path/inside/the/shipped/diff.ts',
75
+ 'path/inside/the/surface.ts',
76
+ 'apps/x/specs/...'
77
+ ])
78
+
79
+ /**
80
+ * A cited path, inline-backtick-delimited, shaped like a repo-relative
81
+ * path: starts with a word/dot/dash character (never `/` — that shape is a
82
+ * web route, e.g. `` `/docs/state-machine` ``, not a repo path), contains at
83
+ * least one `/`, and carries no character a URL or a prose fragment would
84
+ * (no `:`, no whitespace, no parens) — which also excludes a link like
85
+ * `` `https://vinaya.dev` `` without a separate URL-shaped exclusion.
86
+ */
87
+ const CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/
88
+
89
+ /**
90
+ * Top segments that are never a repo path, verified against every one of
91
+ * their occurrences in the real corpus rather than assumed: `origin/`/`refs/`
92
+ * are git-ref namespace (`origin/main`, `refs/pull/N/merge`), and `vinaya/`/
93
+ * `fix/` are this doctrine's own GitHub-label and branch-name-example
94
+ * conventions (`vinaya/blocked`, `fix/brief-gate-nontask`) — a citation kind
95
+ * this check does not judge at all, portable or not, the same way it never
96
+ * tries to also judge a forge number or a tranche slug.
97
+ *
98
+ * **A closed, tested set, not a growable escape hatch.** Excluding a top
99
+ * segment here means every citation under it is invisible to this check —
100
+ * never flagged, however non-portable it would otherwise be — so this set
101
+ * carries exactly the corpus-verified exceptions above and nothing else.
102
+ * `doctrine-portability.test.ts` locks its exact membership; an addition
103
+ * that doesn't also update that lock is a bug, not a silent expansion.
104
+ * Exported for that lock, not for callers to extend at runtime.
105
+ */
106
+ export const NON_PATH_TOP_SEGMENTS: ReadonlySet<string> = new Set(['origin', 'refs', 'HEAD', 'vinaya', 'fix'])
107
+
108
+ /** Every inline-backtick span in `content`, tested against `CITED_PATH_PATTERN`. */
109
+ function extractCitedPaths(content: string): { cited: string; index: number }[] {
110
+ const found: { cited: string; index: number }[] = []
111
+ const spanPattern = /`([^`\n]+)`/g
112
+ let match: RegExpExecArray | null = spanPattern.exec(content)
113
+ while (match !== null) {
114
+ const cited = match[1] ?? ''
115
+ const topSegment = cited.slice(0, cited.indexOf('/'))
116
+ if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
117
+ found.push({ cited, index: match.index })
118
+ }
119
+ match = spanPattern.exec(content)
120
+ }
121
+ return found
122
+ }
123
+
124
+ function lineAtIndex(content: string, index: number): number {
125
+ let line = 1
126
+ for (let i = 0; i < index; i++) {
127
+ if (content.charCodeAt(i) === 10) line++
128
+ }
129
+ return line
130
+ }
131
+
132
+ /**
133
+ * True iff `cited`'s top segment is portable: the caller-supplied
134
+ * `shipsPrefix` itself (doctrine citing its own tree, wherever that tree
135
+ * actually lives for this caller), or one of the static portable prefixes.
136
+ */
137
+ function isPortable(cited: string, shipsPrefix: string): boolean {
138
+ if (cited.startsWith(shipsPrefix)) return true
139
+ return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix))
140
+ }
141
+
142
+ /**
143
+ * Sweeps every file under `shipsPrefix` for a cited path whose top segment
144
+ * is not allow-listed portable. Files outside `shipsPrefix` are out of
145
+ * scope entirely — this check only judges what the shipped doctrine tree
146
+ * itself cites, never a repo's other source.
147
+ */
148
+ export function checkDoctrinePortability(
149
+ files: readonly PortabilitySourceFile[],
150
+ shipsPrefix: string = DEFAULT_SHIPS_PREFIX
151
+ ): PortabilityFinding[] {
152
+ const findings: PortabilityFinding[] = []
153
+
154
+ for (const file of files) {
155
+ if (!file.path.startsWith(shipsPrefix)) continue
156
+
157
+ for (const { cited, index } of extractCitedPaths(file.content)) {
158
+ if (EXEMPT_LITERALS.has(cited)) continue
159
+ if (isPortable(cited, shipsPrefix)) continue
160
+ findings.push({
161
+ file: file.path,
162
+ line: lineAtIndex(file.content, index),
163
+ cited,
164
+ message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`
165
+ })
166
+ }
167
+ }
168
+
169
+ return findings
170
+ }
@@ -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
+ }