@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,320 @@
1
+ /**
2
+ * Reader-resolvable prose — classes 1 and 2 of the three-class analysis in
3
+ * Issue #694: the two mechanizable classes of "words a reader cannot
4
+ * resolve." Class 3 (register/slop — padding adjectives, first person,
5
+ * narrating the work episode) is NOT here; it stays with the review role,
6
+ * per the measurement below.
7
+ *
8
+ * **The measurement that disqualifies a phrase blacklist.** A phrase
9
+ * blacklist tested against the real corpus (all of `aeg-root` plus 1,157
10
+ * TypeScript files) scored 14 hits, 14 false positives, 0 true positives —
11
+ * "a review was *requested*"; "could *this change* leak a secret"; "*I have*
12
+ * X, Y, Z" inside a quoted example. The fix is not tuning the word list; it
13
+ * is asking a different question:
14
+ *
15
+ * - Class 1 (references): not "does this phrase appear" but "does this
16
+ * doctrine cite a forge number or tranche slug the reader has no forge to
17
+ * resolve." Reuses the same two pattern shapes proven in
18
+ * `retired-vocabulary.test.ts` (`FORGE_NUMBER_PATTERN`,
19
+ * `TRANCHE_SLUG_VN_PATTERN`, and the legacy-slug list derived — not
20
+ * guessed — from `aeg-root/tranches/completed/*.md` filenames), scoped to
21
+ * the reader-facing surfaces instead of banned repo-wide (the same digit
22
+ * shape is this product's own live `Closes #N`/PR-number grammar
23
+ * elsewhere, so a repo-wide ban would flag the mechanism, not the
24
+ * citation habit).
25
+ * - Class 2 (vocabulary): not "is this word forbidden" but "does this
26
+ * reader-facing file use a coined term while neither defining it inline
27
+ * nor linking a definition." The term list is derived from
28
+ * `aeg-root/glossary.md`'s own entry headings — never hard-coded — so it
29
+ * cannot drift the first time an entry is added.
30
+ *
31
+ * Zero I/O: every input (file paths + contents, the glossary's term list, the
32
+ * legacy-slug list) is read by the adapter and passed in. POSIX-ERE-safe
33
+ * patterns only — no `\d`/`\w`/`\s`/lookaheads, and no `\b` relied on after a
34
+ * non-word character (the exact regression class `retired-vocabulary.test.ts`
35
+ * already paid for) — even though these patterns run through native
36
+ * `RegExp`, not `grep -E`, keeping them portable to either.
37
+ */
38
+
39
+ export type ProseFileClass = 'ships' | 'reader-facing' | 'internal'
40
+
41
+ export type ProseSourceFile = { path: string; content: string }
42
+
43
+ export type ProseFinding = {
44
+ file: string
45
+ line: number
46
+ message: string
47
+ }
48
+
49
+ /**
50
+ * `aeg-root/**` by default — what this repo's own package carries and
51
+ * `/docs` publishes. Every caller below takes `shipsPrefix` as its LAST,
52
+ * defaulted parameter so an adopter whose installed doctrine tree lives
53
+ * somewhere else (task 7, Issue #56 — de-hardcoding `doctrineRoot`) can
54
+ * override it without breaking any existing positional call.
55
+ */
56
+ const SHIPS_PREFIX = 'aeg-root/'
57
+
58
+ /** `<shipsPrefix>tranches/completed/` — history, never rewritten. Same exemption `retired-vocabulary.test.ts` already carries for the identical reason: an archived tranche legitimately cites the forge numbers and slugs it closed. */
59
+ function shipsArchivePrefix(shipsPrefix: string): string {
60
+ return `${shipsPrefix}tranches/completed/`
61
+ }
62
+
63
+ /** A per-product specs file (`apps/<product>/specs/**`) — this reader has this forge; references are legitimate here. */
64
+ function isSpecFile(path: string): boolean {
65
+ return path.startsWith('apps/') && path.includes('/specs/') && path.endsWith('.md')
66
+ }
67
+
68
+ /** Any `CLAUDE.md`, at any depth — same reasoning as specs: this reader has this forge. */
69
+ function isClaudeMdFile(path: string): boolean {
70
+ return path === 'CLAUDE.md' || path.endsWith('/CLAUDE.md')
71
+ }
72
+
73
+ /**
74
+ * The file-class map, encoded as data (not a rule buried in control flow).
75
+ * Returns `null` for anything outside all three classes — out of this
76
+ * check's scope entirely, not merely exempt.
77
+ *
78
+ * `readerFacingPrefix`/`readerFacingSuffix` scope the "reader-facing" class
79
+ * to a consumer's own reader-facing surface — this package stays generic and
80
+ * has no attalabs-specific path baked in. The caller-supplied shape mirrors
81
+ * the vinaya adopter's product site: a page's own body is where
82
+ * reader-visible prose lives (JSX text, step copy); sibling component
83
+ * directories include non-textual/decorative surfaces (e.g. canvas
84
+ * illustrations using fictional PR numbers as flavor text) that are not
85
+ * prose in the sense this check means, hence a suffix-scoped match rather
86
+ * than a bare prefix.
87
+ */
88
+ export function classifyProseFile(
89
+ path: string,
90
+ readerFacingPrefix: string,
91
+ readerFacingSuffix: string,
92
+ shipsPrefix: string = SHIPS_PREFIX
93
+ ): ProseFileClass | null {
94
+ if (path.startsWith(shipsPrefix)) {
95
+ return path.startsWith(shipsArchivePrefix(shipsPrefix)) ? 'internal' : 'ships'
96
+ }
97
+ if (path.startsWith(readerFacingPrefix) && path.endsWith(readerFacingSuffix)) {
98
+ return 'reader-facing'
99
+ }
100
+ if (isSpecFile(path) || isClaudeMdFile(path)) return 'internal'
101
+ return null
102
+ }
103
+
104
+ /** The two classes this check actually sweeps — `internal` never is. */
105
+ const SWEPT_CLASSES: ReadonlySet<ProseFileClass> = new Set(['ships', 'reader-facing'])
106
+
107
+ /**
108
+ * Strips the parts of a file that are never reader prose, per file kind — a
109
+ * fenced/inline code example in a markdown doc, or a source comment in a
110
+ * `.tsx` page (a page's own developer-facing comments, e.g. "same reasoning
111
+ * as `/roadmap`", are not what a site visitor reads). Line count is
112
+ * preserved (replacements keep their newlines) so reported line numbers stay
113
+ * accurate against the original file.
114
+ *
115
+ * The `//` line-comment cut requires the marker not be immediately preceded
116
+ * by `:` — without that, `https://vinaya.dev` on a reader-facing line reads
117
+ * as a comment starting at its own `//`, silently blanking every word of
118
+ * real prose that follows it on the line. No boundary check is needed on
119
+ * the far side: unlike `//`, the character before it is never itself part
120
+ * of the marker, so a plain capturing group (not a lookbehind) is enough.
121
+ */
122
+ export function stripNonProse(path: string, content: string): string {
123
+ if (path.endsWith('.md')) {
124
+ return content
125
+ .replace(/```.*?```/gs, (m) => m.replace(/[^\n]/g, ''))
126
+ .replace(/`[^`\n]*`/g, (m) => m.replace(/[^\n]/g, ''))
127
+ }
128
+ if (path.endsWith('.tsx') || path.endsWith('.ts')) {
129
+ return content
130
+ .replace(/\/\*.*?\*\//gs, (m) => m.replace(/[^\n]/g, ''))
131
+ .replace(/(^|[^:])\/\/[^\n]*/g, (m) => m.replace(/[^\n]/g, ''))
132
+ }
133
+ return content
134
+ }
135
+
136
+ function lineAt(content: string, index: number): number {
137
+ let line = 1
138
+ for (let i = 0; i < index; i++) {
139
+ if (content.charCodeAt(i) === 10) line++
140
+ }
141
+ return line
142
+ }
143
+
144
+ /** `#294`-shaped — a PR/Issue number cited bare, resolvable only inside this repo's own tracker. */
145
+ export const FORGE_NUMBER_PATTERN = /#[0-9]{2,4}/g
146
+
147
+ /** `aeg-forge-state-v1`-shaped — an internal tranche slug ending `-vN`. */
148
+ export const TRANCHE_SLUG_VN_PATTERN = /[a-z][a-z-]+-v[0-9]/g
149
+
150
+ /**
151
+ * Builds the legacy-slug pattern from the real archived-tranche filenames
152
+ * the adapter reads (mirrors `retired-vocabulary.test.ts`'s `legacySlugs()` —
153
+ * derived from `aeg-root/tranches/completed/*.md`, never a shape guess).
154
+ * Returns `null` when there is nothing left for it to catch (every archived
155
+ * slug now ends `-vN`), matching that file's "no pattern with zero positive
156
+ * samples" rule.
157
+ *
158
+ * Boundary-checked like `containsWholeWord` below (character class, not
159
+ * `\b`) so a real slug cannot match as a bare substring inside a longer,
160
+ * unrelated token. `retired-vocabulary.test.ts`'s own copy of this pattern
161
+ * has no such boundary; this one adds it rather than copying the gap.
162
+ * Group 1 is the leading boundary char (or start-of-string), group 2 is the
163
+ * slug itself, group 3 the trailing boundary char (or end-of-string) — the
164
+ * caller reports group 2, not the whole match, so the boundary chars never
165
+ * leak into a finding's message.
166
+ */
167
+ export function legacySlugPattern(legacySlugs: readonly string[]): RegExp | null {
168
+ if (legacySlugs.length === 0) return null
169
+ const alternation = legacySlugs.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
170
+ return new RegExp(`(^|[^a-zA-Z0-9\\n-])(${alternation})([^a-zA-Z0-9\\n-]|$)`, 'g')
171
+ }
172
+
173
+ /**
174
+ * Class 1 — unresolvable references. Runs the reference patterns over every
175
+ * swept file (`ships`/`reader-facing`), skipping code-fenced/commented-out
176
+ * non-prose first.
177
+ */
178
+ export function checkUnresolvableReferences(
179
+ files: readonly ProseSourceFile[],
180
+ readerFacingPrefix: string,
181
+ readerFacingSuffix: string,
182
+ legacySlugs: readonly string[] = [],
183
+ shipsPrefix: string = SHIPS_PREFIX
184
+ ): ProseFinding[] {
185
+ const findings: ProseFinding[] = []
186
+ const legacyPattern = legacySlugPattern(legacySlugs)
187
+ // `group` names the capture holding the actual cited text — the plain
188
+ // patterns have none (report the whole match), the boundary-checked
189
+ // legacy-slug pattern reports its group 2 so the boundary chars around it
190
+ // never leak into the finding's message.
191
+ const patterns: { pattern: RegExp; what: string; group?: number }[] = [
192
+ { pattern: FORGE_NUMBER_PATTERN, what: 'a forge number' },
193
+ { pattern: TRANCHE_SLUG_VN_PATTERN, what: 'an internal tranche slug' },
194
+ ...(legacyPattern ? [{ pattern: legacyPattern, what: 'an internal tranche slug', group: 2 }] : [])
195
+ ]
196
+
197
+ for (const file of files) {
198
+ const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
199
+ if (!cls || !SWEPT_CLASSES.has(cls)) continue
200
+ const scrubbed = stripNonProse(file.path, file.content)
201
+ for (const { pattern, what, group } of patterns) {
202
+ pattern.lastIndex = 0
203
+ let match: RegExpExecArray | null = pattern.exec(scrubbed)
204
+ while (match !== null) {
205
+ const cited = group !== undefined ? (match[group] ?? match[0]) : match[0]
206
+ findings.push({
207
+ file: file.path,
208
+ line: lineAt(scrubbed, match.index),
209
+ message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`
210
+ })
211
+ match = pattern.exec(scrubbed)
212
+ }
213
+ }
214
+ }
215
+ return findings
216
+ }
217
+
218
+ /** Escapes a glossary term for use inside a hand-built, boundary-checked pattern. */
219
+ function escapeTerm(term: string): string {
220
+ return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
221
+ }
222
+
223
+ /**
224
+ * Whole-word (or whole-phrase), case-insensitive occurrence of `term` in
225
+ * `text` — boundaries checked by character class rather than `\b`, per the
226
+ * "do not rely on `\b` after a non-word character" lesson. Matches the exact
227
+ * glossary heading only (no inflections): "briefly" must never match
228
+ * "brief" the way a prefix match would — that exact shape (`let's` matching
229
+ * `bullet's`) is what the disqualifying measurement caught.
230
+ */
231
+ function containsWholeWord(text: string, term: string): boolean {
232
+ const escaped = escapeTerm(term)
233
+ const re = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)`, 'i')
234
+ return re.test(text)
235
+ }
236
+
237
+ function findWholeWordLine(content: string, term: string): number | null {
238
+ const lines = content.split('\n')
239
+ let lineNo = 0
240
+ for (const line of lines) {
241
+ lineNo++
242
+ if (containsWholeWord(line, term)) return lineNo
243
+ }
244
+ return null
245
+ }
246
+
247
+ /**
248
+ * A file "defines" a term inline when it carries the same
249
+ * `Term — definition` shape the glossary itself uses (an em-dash right after
250
+ * the term), or links the glossary page/file directly.
251
+ */
252
+ function definesOrLinksGlossary(content: string, term: string): boolean {
253
+ const escaped = escapeTerm(term)
254
+ const definesInline = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)[^\\n]{0,3}—`, 'i').test(content)
255
+ if (definesInline) return true
256
+ return /docs\/glossary|glossary\.md/i.test(content)
257
+ }
258
+
259
+ /**
260
+ * Class 2 — undefined coined vocabulary. Not "is this word forbidden" — it
261
+ * is "does this reader-facing file use a coined term while neither defining
262
+ * it inline nor linking a definition." `glossaryTerms` is read by the
263
+ * adapter from `aeg-root/glossary.md`'s own entry headings — never
264
+ * hard-coded here, so the check cannot silently rot the first time an entry
265
+ * is added.
266
+ */
267
+ export function checkUndefinedVocabulary(
268
+ files: readonly ProseSourceFile[],
269
+ glossaryTerms: readonly string[],
270
+ readerFacingPrefix: string,
271
+ readerFacingSuffix: string,
272
+ shipsPrefix: string = SHIPS_PREFIX
273
+ ): ProseFinding[] {
274
+ const findings: ProseFinding[] = []
275
+
276
+ for (const file of files) {
277
+ const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
278
+ if (!cls || !SWEPT_CLASSES.has(cls)) continue
279
+ const scrubbed = stripNonProse(file.path, file.content)
280
+
281
+ for (const term of glossaryTerms) {
282
+ const line = findWholeWordLine(scrubbed, term)
283
+ if (line === null) continue
284
+ if (definesOrLinksGlossary(scrubbed, term)) continue
285
+ findings.push({
286
+ file: file.path,
287
+ line,
288
+ message: `uses coined term "${term}" without defining it inline or linking the glossary`
289
+ })
290
+ }
291
+ }
292
+ return findings
293
+ }
294
+
295
+ /** Runs both mechanizable classes over the given files in one pass. */
296
+ export function checkReaderResolvableProse(
297
+ files: readonly ProseSourceFile[],
298
+ glossaryTerms: readonly string[],
299
+ readerFacingPrefix: string,
300
+ readerFacingSuffix: string,
301
+ legacySlugs: readonly string[] = [],
302
+ shipsPrefix: string = SHIPS_PREFIX
303
+ ): ProseFinding[] {
304
+ return [
305
+ ...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs, shipsPrefix),
306
+ ...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
307
+ ]
308
+ }
309
+
310
+ /** Parses `aeg-root/glossary.md`'s `**Term** — definition` entry headings into a bare term list. */
311
+ export function parseGlossaryTerms(glossaryContent: string): string[] {
312
+ const terms: string[] = []
313
+ const re = /^\*\*(.+?)\*\* —/gm
314
+ let match: RegExpExecArray | null = re.exec(glossaryContent)
315
+ while (match !== null) {
316
+ if (match[1] !== undefined) terms.push(match[1])
317
+ match = re.exec(glossaryContent)
318
+ }
319
+ return terms
320
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * registry-checks.ts — G1–G6, the deterministic coherence checks that make
3
+ * `aeg-root/enforcement.md`'s three ring tables (parsed by
4
+ * `registry-parse.ts`) load-bearing instead of decorative.
5
+ *
6
+ * Pure — no `fs`, no `git`/`gh` I/O, no `fetch`. All forge/filesystem facts
7
+ * are injected by the caller (`bin/verify-registry.ts` /
8
+ * `apps/cli/src/checks/bin/check-registry-gates.ts`, the I/O shims),
9
+ * mirroring `coherence-checks.ts`'s shape exactly (plain executables,
10
+ * deterministic pass/fail, no config conditionals).
11
+ *
12
+ * Rollout policy: G2 ships report-only — it can only ever report `'info'`,
13
+ * never `'fail'`, so it never affects CI's exit code (the accepted shape for
14
+ * G2's inverse sweep, unchanged by task 8). G1 was report-only through its
15
+ * own rollout window; task 8 flips it to blocking now that the orphan
16
+ * backlog it existed to surface is clean (0 findings against the real
17
+ * `enforcement.md` at the time of the flip). G3–G6 are blocking (`'fail'` on
18
+ * any violation).
19
+ */
20
+
21
+ import { GATE_AUDIENCE, isShipped } from './gate-audience'
22
+ import type { GateRow } from './registry-parse'
23
+
24
+ export type RegistryCheckStatus = 'pass' | 'fail' | 'info'
25
+
26
+ export type RegistryFinding = {
27
+ row?: string
28
+ path?: string
29
+ reason: string
30
+ }
31
+
32
+ export type RegistryCheckResult = {
33
+ check: 'G1' | 'G2' | 'G3' | 'G4' | 'G5' | 'G6'
34
+ status: RegistryCheckStatus
35
+ findings: RegistryFinding[]
36
+ }
37
+
38
+ /**
39
+ * G1 — every row's non-empty `implementation` resolves on disk. Blocking as
40
+ * of task 8 (re-graded from report-only: eleven permanent `info` findings on
41
+ * every run had become indistinguishable from silence, which is how the gap
42
+ * this tranche closes stayed invisible; the report-only window had already
43
+ * cleared the backlog it existed to surface).
44
+ */
45
+ export function checkG1(rows: GateRow[], existsFn: (path: string) => boolean): RegistryCheckResult {
46
+ const findings: RegistryFinding[] = []
47
+ for (const row of rows) {
48
+ if (row.implementation === '') continue
49
+ if (!existsFn(row.implementation)) {
50
+ findings.push({
51
+ row: row.action,
52
+ path: row.implementation,
53
+ reason: `${row.ring} row "${row.action}" names implementation "${row.implementation}", which does not exist on disk`
54
+ })
55
+ }
56
+ }
57
+ return { check: 'G1', status: findings.length > 0 ? 'fail' : 'pass', findings }
58
+ }
59
+
60
+ /** The literal placeholder marker a registry-scaffold stub row's
61
+ * non-mechanical cells carry (`registry-scaffold.ts`'s `PLACEHOLDER`,
62
+ * duplicated here rather than imported to keep this module's zero-import
63
+ * shape — the two are asserted equal by `registry-checks.test.ts`). A stub
64
+ * that filled `implementation` alone would silence G2 outright (the whole
65
+ * defect Issue #104 exists to close): this marker is what keeps a row G2
66
+ * still flags loud until a human replaces it with the real "why". */
67
+ const SCAFFOLD_PLACEHOLDER = '[undocumented — fill in why]'
68
+
69
+ /**
70
+ * G2 — every candidate hook/CLI file is named by SOME row's `implementation`,
71
+ * AND no row still carries the scaffold's placeholder marker un-replaced.
72
+ * `candidateFiles` is already-globbed by the caller (`.husky/*`,
73
+ * `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts`, excluding `.husky/_`).
74
+ * Report-only this tranche — same as G1, never `'fail'`.
75
+ *
76
+ * The second half (placeholder scan) is what keeps a registry-scaffold stub
77
+ * row visible: `implementation` presence alone would make a stub read as
78
+ * "documented" to the ORPHAN half above, which is precisely the trap the
79
+ * scaffold must not fall into. Scanned over `summary`/`description`/`spec`
80
+ * — every cell a stub can carry the marker in — never `implementation`,
81
+ * `action`, `category`, or `audience`, which the scaffold fills mechanically
82
+ * and are never expected to carry it.
83
+ */
84
+ export function checkG2(rows: GateRow[], candidateFiles: string[]): RegistryCheckResult {
85
+ const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ''))
86
+ const findings: RegistryFinding[] = []
87
+ for (const path of candidateFiles) {
88
+ if (!implementations.has(path)) {
89
+ findings.push({
90
+ path,
91
+ reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
92
+ })
93
+ }
94
+ }
95
+ for (const row of rows) {
96
+ const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER)
97
+ if (carriesPlaceholder) {
98
+ findings.push({
99
+ row: row.action,
100
+ path: row.implementation,
101
+ reason: `${row.ring} row "${row.action}" still carries the scaffold placeholder marker — the why is owed`
102
+ })
103
+ }
104
+ }
105
+ return { check: 'G2', status: findings.length > 0 ? 'info' : 'pass', findings }
106
+ }
107
+
108
+ /**
109
+ * G3 — every file that makes a GitHub-crossing call is named by SOME Ring-0
110
+ * row's `implementation` — "no seventh way into GitHub". `crossingFiles` is
111
+ * already-detected by the caller via a source-grep. Blocking.
112
+ */
113
+ export function checkG3(ring0Rows: GateRow[], crossingFiles: string[]): RegistryCheckResult {
114
+ const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation))
115
+ const findings: RegistryFinding[] = []
116
+ for (const path of crossingFiles) {
117
+ if (!ring0Implementations.has(path)) {
118
+ findings.push({
119
+ path,
120
+ reason: `"${path}" makes a GitHub-crossing call but is not named by any Ring-0 row's implementation — a seventh, unlisted way into GitHub`
121
+ })
122
+ }
123
+ }
124
+ return { check: 'G3', status: findings.length > 0 ? 'fail' : 'pass', findings }
125
+ }
126
+
127
+ /**
128
+ * G4 — every `#NNN` cited in enforcement.md's body resolves in the forge.
129
+ * `resolveFn` wraps `gh issue view`/`gh pr view` (caller injects). Blocking.
130
+ * Broad reading (brief): every `#`-prefixed 3-or-more-digit number
131
+ * occurring anywhere in the body is checked, not just ones near keywords
132
+ * like "incident" — an incomplete keyword net would create a
133
+ * false-negative gap. The current repo's real citations are all 3-4
134
+ * digits; the lower bound of 3 exists only to exclude unrelated short
135
+ * numerals (ring/tier numbers) that are never written with a `#` prefix
136
+ * anyway — there is deliberately no upper bound, so a longer fabricated
137
+ * number is still caught.
138
+ */
139
+ export function checkG4(content: string, resolveFn: (n: number) => boolean): RegistryCheckResult {
140
+ const cited = new Set<number>()
141
+ for (const hit of content.matchAll(/#(\d{3,})\b/g)) {
142
+ cited.add(Number(hit[1]))
143
+ }
144
+ const findings: RegistryFinding[] = []
145
+ for (const n of cited) {
146
+ if (!resolveFn(n)) {
147
+ findings.push({
148
+ reason: `#${n} is cited in enforcement.md but does not resolve to a real Issue or PR in the forge`
149
+ })
150
+ }
151
+ }
152
+ return { check: 'G4', status: findings.length > 0 ? 'fail' : 'pass', findings }
153
+ }
154
+
155
+ /**
156
+ * G5 — every contract's producer/consumer is a real role_id; every role's
157
+ * `performs`/`refuses_when` is present and non-empty. Blocking.
158
+ *
159
+ * "The action exists" is satisfied by presence/well-formedness of the
160
+ * role's own frontmatter — there is no second, independent registry of
161
+ * valid actions to cross-reference `performs` entries against.
162
+ */
163
+ export function checkG5(
164
+ roles: Array<{ file: string; role_id: string; performs: string[]; refuses_when: string }>,
165
+ contracts: Array<{ file: string; producer: string; consumer: string }>
166
+ ): RegistryCheckResult {
167
+ const roleIds = new Set(roles.map((r) => r.role_id))
168
+ const findings: RegistryFinding[] = []
169
+
170
+ for (const contract of contracts) {
171
+ if (!roleIds.has(contract.producer)) {
172
+ findings.push({
173
+ path: contract.file,
174
+ reason: `contract "${contract.file}" names producer "${contract.producer}", which is not a real role_id`
175
+ })
176
+ }
177
+ if (!roleIds.has(contract.consumer)) {
178
+ findings.push({
179
+ path: contract.file,
180
+ reason: `contract "${contract.file}" names consumer "${contract.consumer}", which is not a real role_id`
181
+ })
182
+ }
183
+ }
184
+
185
+ for (const role of roles) {
186
+ if (role.performs.length === 0) {
187
+ findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty performs array` })
188
+ }
189
+ if (role.refuses_when.trim() === '') {
190
+ findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty refuses_when` })
191
+ }
192
+ }
193
+
194
+ return { check: 'G5', status: findings.length > 0 ? 'fail' : 'pass', findings }
195
+ }
196
+
197
+ const SHIPPED_BIN_PATH_PREFIX = 'apps/cli/src/checks/bin/check-'
198
+ const AEG_CORE_BIN_PATH_PREFIX = 'packages/aeg-core/bin/'
199
+
200
+ function basenameNoExt(path: string): string {
201
+ const base = path.slice(path.lastIndexOf('/') + 1)
202
+ return base.replace(/\.ts$/, '')
203
+ }
204
+
205
+ /**
206
+ * Every `coreCheckRegistry()` name a `product`-audience row's implementation
207
+ * could resolve to. Two shapes: a path directly under the shipped bin
208
+ * directory names its own check 1:1 (`apps/cli/src/checks/bin/check-doc-
209
+ * coverage-push.ts` -> `doc-coverage-push`); a path under
210
+ * `packages/aeg-core/bin/` resolves through `GATE_AUDIENCE`, the same map
211
+ * `shipped-bin-audience.test.ts` (`apps/cli`) already asserts agrees with
212
+ * the registry — reused here rather than re-derived, so doctrine and code
213
+ * cannot silently disagree about what a bin ships as. Neither shape matches
214
+ * (e.g. a `.github/workflows/*.yml` path, `checks/runner.ts`, a forge-write
215
+ * command) resolves to no candidate at all — correct, since those are not
216
+ * `coreCheckRegistry()` checks by construction.
217
+ */
218
+ function claimedCheckNames(implementation: string): string[] {
219
+ if (implementation.startsWith(SHIPPED_BIN_PATH_PREFIX) && implementation.endsWith('.ts')) {
220
+ return [implementation.slice(SHIPPED_BIN_PATH_PREFIX.length, -'.ts'.length)]
221
+ }
222
+ if (implementation.startsWith(AEG_CORE_BIN_PATH_PREFIX)) {
223
+ const audience = GATE_AUDIENCE[basenameNoExt(implementation)]
224
+ if (audience && isShipped(audience)) {
225
+ return Array.isArray(audience.shippedAs) ? audience.shippedAs : [audience.shippedAs]
226
+ }
227
+ }
228
+ return []
229
+ }
230
+
231
+ /**
232
+ * G6 — every row doctrine marks `product` must actually resolve to a
233
+ * `coreCheckRegistry()` entry ("claimed checks must ship" — the parity gate
234
+ * this task exists for). `registeredCheckNames` is caller-injected
235
+ * (`coreCheckRegistry().map(s => s.name)`, from `apps/cli`, which
236
+ * `aeg-core` cannot import without closing a dependency cycle — same
237
+ * reasoning `gate-audience.ts` documents for `GATE_AUDIENCE` itself).
238
+ * Blocking. A row left at the default `repo-own` audience makes no shipped
239
+ * claim and is never checked here.
240
+ */
241
+ export function checkG6(rows: GateRow[], registeredCheckNames: Set<string>): RegistryCheckResult {
242
+ const findings: RegistryFinding[] = []
243
+ for (const row of rows) {
244
+ if (row.audience !== 'product') continue
245
+ const candidates = claimedCheckNames(row.implementation)
246
+ if (!candidates.some((name) => registeredCheckNames.has(name))) {
247
+ findings.push({
248
+ row: row.action,
249
+ path: row.implementation,
250
+ reason: `${row.ring} row "${row.action}" is marked \`product\` but its implementation "${row.implementation}" does not resolve to a coreCheckRegistry() entry`
251
+ })
252
+ }
253
+ }
254
+ return { check: 'G6', status: findings.length > 0 ? 'fail' : 'pass', findings }
255
+ }