@attalabs/vinaya 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +1 -0
  2. package/dist/checks/bin/check-body-bare-digits.js +7 -1
  3. package/dist/checks/bin/check-branch-topology.js +27 -1
  4. package/dist/checks/bin/check-brief-shape.js +7 -1
  5. package/dist/checks/bin/check-changeset-coverage.js +7 -1
  6. package/dist/checks/bin/check-closes-n.js +27 -1
  7. package/dist/checks/bin/check-coherence.js +27 -1
  8. package/dist/checks/bin/check-dead-branch-push.js +7 -1
  9. package/dist/checks/bin/check-dispatch-readiness.js +27 -1
  10. package/dist/checks/bin/check-doc-coverage-push.js +7 -1
  11. package/dist/checks/bin/check-doc-coverage.js +7 -1
  12. package/dist/checks/bin/check-doctrine-no-procedures.js +7 -1
  13. package/dist/checks/bin/check-doctrine-portability.js +7 -1
  14. package/dist/checks/bin/check-evidence-fresh.js +7 -1
  15. package/dist/checks/bin/check-exec-bits.js +7 -1
  16. package/dist/checks/bin/check-first-push-dispatch.js +27 -1
  17. package/dist/checks/bin/check-issue-assignment.js +27 -1
  18. package/dist/checks/bin/check-main-branch-refusal.js +13 -2
  19. package/dist/checks/bin/check-no-disk-state.js +7 -1
  20. package/dist/checks/bin/check-pr-report-density.js +7 -1
  21. package/dist/checks/bin/check-quoted-command.js +38 -12
  22. package/dist/checks/bin/check-reader-resolvable-prose.js +38 -12
  23. package/dist/checks/bin/check-registry-gates.js +7 -1
  24. package/dist/checks/bin/check-retired-vocabulary.js +38 -12
  25. package/dist/checks/bin/check-review-gate.js +7 -1
  26. package/dist/checks/bin/check-single-plan-pr.js +7 -1
  27. package/dist/checks/bin/check-test-plan.js +7 -1
  28. package/dist/checks/bin/check-token-collection-wired.js +7 -1
  29. package/dist/checks/bin/check-token-report.js +7 -1
  30. package/dist/checks/bin/check-workspace-escape.js +7 -1
  31. package/dist/index.js +362 -87
  32. package/package.json +1 -1
  33. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +9 -4
  34. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +7 -1
  35. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +462 -0
  36. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +405 -4
  37. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +19 -0
  38. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/consumer-enumeration.ts +75 -0
  39. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +44 -0
  40. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-no-procedures.ts +94 -0
  41. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +128 -5
  42. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +3 -0
  43. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +25 -3
  44. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +17 -5
  45. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-report-density.ts +116 -0
  46. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +35 -0
  47. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +3 -3
  48. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +116 -7
  49. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-status.ts +200 -0
  50. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +25 -12
  51. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +70 -11
  52. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  54. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  91. 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
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1mcitst._.js → [root-of-the-server]__1pndh3_._.js} +1 -1
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +3 -3
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  110. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0jer_drkl1e4r.js → 0zebsmmk1bvnb.js} +4 -4
  111. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{20x-t0a0v129z.js → 1uogpj5w2n5ju.js} +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2ou_81ahqw31e.js → 3m1kgax7j2vgs.js} +1 -1
  113. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  114. /package/studio-standalone/_node_modules/@attalabs/vinaya/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
  115. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_buildManifest.js +0 -0
  116. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_clientMiddlewareManifest.js +0 -0
  117. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_ssgManifest.js +0 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Doctrine-no-procedures sweep (task 10, Issue #385) — task 9's rule that
3
+ * doctrine prose describes no command sequence, made a check. `roles/*.md`
4
+ * and `contracts/*.md` explain *what* a command sequence does and *why*;
5
+ * they are not a runbook a reader executes verbatim, and a copy-pasted
6
+ * sequence rots the moment the real command changes (found live: two of
7
+ * this tranche's own PRs were blocked writing exactly this shape after the
8
+ * rule was already known in prose).
9
+ *
10
+ * Zero I/O: every input (file paths + contents) is read by the adapter
11
+ * (`check-doctrine-no-procedures.ts`) and passed in — same discipline as
12
+ * `doctrine-portability.ts` beside it.
13
+ */
14
+
15
+ import { COMMAND_WORDS, extractFencedBlocks } from './brief-validation'
16
+ import { VENDOR_EXAMPLE_END, VENDOR_EXAMPLE_START } from './doctrine-portability'
17
+
18
+ export type DoctrineFile = { path: string; content: string }
19
+ export type DoctrineProcedureFinding = { file: string; line: number; message: string }
20
+
21
+ /**
22
+ * The one sanctioned fenced home for a real command sequence in doctrine
23
+ * (`tranche-model.md` §12's `AEG:VENDOR-EXAMPLE` pair) — same markers
24
+ * `doctrine-portability.ts` exempts from its own vendor-name scan, exported
25
+ * from there rather than re-declared here. First pair wins, same rule as
26
+ * every other `AEG:*` anchor consumer.
27
+ */
28
+ function vendorExampleSpan(content: string): { start: number; end: number } | null {
29
+ const start = VENDOR_EXAMPLE_START.exec(content)
30
+ if (!start) return null
31
+ const afterStart = start.index + start[0].length
32
+ const end = VENDOR_EXAMPLE_END.exec(content.slice(afterStart))
33
+ if (!end) return null
34
+ return { start: start.index, end: afterStart + end.index + end[0].length }
35
+ }
36
+
37
+ function isCommandLine(line: string): boolean {
38
+ const word = line.trim().split(/\s+/)[0] ?? ''
39
+ return (COMMAND_WORDS as readonly string[]).includes(word)
40
+ }
41
+
42
+ /**
43
+ * A fence's language tag that reads as shell — the empty tag (` ``` `, most
44
+ * doctrine command blocks) or an explicit `sh`/`bash`/`shell`/`console`.
45
+ * Anything else (`ts`, `js`, `yaml`, `json`, …) is a real illustration in
46
+ * that language, not a command sequence — a two-line ` ```ts ` block with two
47
+ * `export` statements is TypeScript, not shell, even though `export` is also
48
+ * a shell builtin (round-2 ruling item 2, found live: this task's own
49
+ * `brief-validation.ts` excerpt in `state-machine.md` reads exactly this
50
+ * way). A block outside this set never reaches the command-line count at
51
+ * all — it is not a candidate, regardless of its content.
52
+ */
53
+ const SHELL_LANGS: ReadonlySet<string> = new Set(['', 'sh', 'bash', 'shell', 'console'])
54
+
55
+ function procedureLineNumberAt(content: string, index: number): number {
56
+ return content.slice(0, index).split('\n').length
57
+ }
58
+
59
+ /** `aeg-root/templates/**` (or any adopter's `<doctrineRoot>/templates/**`) — worked-example templates that show the vendor-facing PR-report shape and legitimately carry a full command block. */
60
+ function isTemplatePath(path: string): boolean {
61
+ return /(^|\/)templates\//.test(path)
62
+ }
63
+
64
+ /**
65
+ * A fenced block containing two or more lines that each start with a shell
66
+ * command word is a command sequence, not an illustration — the failure
67
+ * names it: "this sequence is a `vinaya` command, name it." Exempt: a block
68
+ * inside the `AEG:VENDOR-EXAMPLE` anchor pair, and any file under a
69
+ * `templates/` directory.
70
+ */
71
+ export function checkDoctrineNoProcedures(files: DoctrineFile[]): DoctrineProcedureFinding[] {
72
+ const findings: DoctrineProcedureFinding[] = []
73
+
74
+ for (const file of files) {
75
+ if (isTemplatePath(file.path)) continue
76
+ const span = vendorExampleSpan(file.content)
77
+
78
+ for (const block of extractFencedBlocks(file.content)) {
79
+ if (span && block.start >= span.start && block.end <= span.end) continue
80
+ if (!SHELL_LANGS.has(block.lang)) continue
81
+
82
+ const commandLineCount = block.content.split('\n').filter(isCommandLine).length
83
+ if (commandLineCount >= 2) {
84
+ findings.push({
85
+ file: file.path,
86
+ line: procedureLineNumberAt(file.content, block.start),
87
+ message: `this fenced block has ${commandLineCount} shell-command lines — this sequence is a \`vinaya\` command, name it (or move it inside the \`AEG:VENDOR-EXAMPLE\` anchor / a \`templates/\` file).`
88
+ })
89
+ }
90
+ }
91
+ }
92
+
93
+ return findings
94
+ }
@@ -17,10 +17,31 @@
17
17
  * from), where a deny-list of "known author-repo prefixes" would fail open
18
18
  * on the next unlisted one instead.
19
19
  *
20
+ * **A second, additive dimension (Issue #298): a portable path is not the
21
+ * only way doctrine couples itself to one vendor.** Prose can name a
22
+ * specific AI company, product, or agent directly — "Claude Code", "GPT",
23
+ * "Anthropic" — with no path shape for the path-based predicate above to
24
+ * even look at. `checkDoctrinePortability` now emits a SECOND finding kind
25
+ * for exactly that: a fixed vendor-name word list, scanned against prose
26
+ * (never against code — a masked, `maskCode`-blind scan, the identical
27
+ * code-recognition grammar `anchored-region.ts` uses for the `AEG:*` PR/Issue
28
+ * body anchors, imported directly rather than re-implemented), with exactly
29
+ * ONE standing exemption: text inside a `<!-- AEG:VENDOR-EXAMPLE:START -->` /
30
+ * `<!-- AEG:VENDOR-EXAMPLE:END -->` pair — doctrine's one sanctioned, fenced
31
+ * home for naming today's shipped reference host by product name
32
+ * (`tranche-model.md` §12). A vendor word that is itself part of an
33
+ * already-portable path citation (`` `.claude/hooks/x.sh` ``, `` `CLAUDE.md` ``)
34
+ * is not re-flagged here: it is inline code, already masked by `maskCode`
35
+ * before the word scan ever runs, and the path-shape predicate above already
36
+ * has an opinion about it. This dimension has no I/O of its own either — same
37
+ * inputs, same adapter.
38
+ *
20
39
  * Zero I/O: every input (file paths + contents) is read by the adapter and
21
40
  * passed in.
22
41
  */
23
42
 
43
+ import { maskCode } from '@attalabs/aeg-forge-state/strip-code'
44
+
24
45
  export type PortabilitySourceFile = { path: string; content: string }
25
46
 
26
47
  export type PortabilityFinding = {
@@ -28,6 +49,8 @@ export type PortabilityFinding = {
28
49
  line: number
29
50
  cited: string
30
51
  message: string
52
+ /** `'path'` — the original non-portable-path predicate. `'vendor-name'` — Issue #298's word-list predicate. */
53
+ kind: 'path' | 'vendor-name'
31
54
  }
32
55
 
33
56
  /** `aeg-root/**` by default — what this repo's own package ships and every adopter installs read-only. */
@@ -105,6 +128,93 @@ const CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/
105
128
  */
106
129
  export const NON_PATH_TOP_SEGMENTS: ReadonlySet<string> = new Set(['origin', 'refs', 'HEAD', 'vinaya', 'fix'])
107
130
 
131
+ /**
132
+ * The vendor-name word list (Issue #298's re-count, live at authoring):
133
+ * every AI company/product/agent name this doctrine's own prose has
134
+ * actually used, as either the shipped reference host or a peer example —
135
+ * a real, hardcoded list for this repo's own corpus, not a growable
136
+ * config surface (brief `fix/doctrine-vendor-neutrality` §10: ship a
137
+ * reasonable hardcoded list, flag generalization for later). Longest
138
+ * alternative first ("claude code" before "claude") so a two-word mention
139
+ * is reported whole rather than as a truncated single-word match followed
140
+ * by a dangling "code".
141
+ *
142
+ * Includes model-TIER names (`opus`/`sonnet`/`haiku`), not just
143
+ * company/product names — a review round on this task's own PR (#338)
144
+ * found the first cut missed exactly this class: `brief-authoring/SKILL.md`
145
+ * and `brief-template.md` named a specific model tier bare, in prose, in
146
+ * three places the path-shape predicate could never see either. `sonnet`
147
+ * and `haiku` are ordinary English words outside this domain — a real
148
+ * false-positive risk for a general-purpose tool, accepted here on the
149
+ * same "reasonable hardcoded list for this repo's own corpus" basis as the
150
+ * rest of this list (verified against the live corpus: no non-vendor use
151
+ * of either word exists in `aeg-root/**` today).
152
+ */
153
+ const VENDOR_NAME_SOURCE =
154
+ '\\bclaude code\\b|\\bclaude\\b|\\banthropic\\b|\\bchatgpt\\b|\\bopenai\\b|\\bgpt\\b|\\bgemini\\b|\\bcodex\\b|\\bgrok\\b|\\bdeepseek\\b|\\bopus\\b|\\bsonnet\\b|\\bhaiku\\b'
155
+
156
+ /**
157
+ * Exported (alongside `VENDOR_EXAMPLE_END` below) so `doctrine-no-procedures.ts`
158
+ * can find the same fenced home's raw-text span — never a second copy of this
159
+ * pattern — to exempt a fenced block that sits inside it, the same way this
160
+ * file exempts the region from its own vendor-name word scan.
161
+ */
162
+ export const VENDOR_EXAMPLE_START = /<!--\s*AEG:VENDOR-EXAMPLE:START\s*-->/
163
+ export const VENDOR_EXAMPLE_END = /<!--\s*AEG:VENDOR-EXAMPLE:END\s*-->/
164
+
165
+ /**
166
+ * Blanks (same-length, index-preserving — same discipline as `maskCode`
167
+ * itself) the region between the first well-formed
168
+ * `<!-- AEG:VENDOR-EXAMPLE:START -->` … `<!-- AEG:VENDOR-EXAMPLE:END -->`
169
+ * pair in `masked` — doctrine's one sanctioned home for naming today's
170
+ * shipped reference host by product name (`tranche-model.md` §12).
171
+ * Markers are searched on already-`maskCode`d text, the same order
172
+ * `anchoredRegionBounds` searches the `AEG:*` PR/Issue-body anchors, so a
173
+ * decoy pair quoted inside a fenced example never wins. A START with no
174
+ * following END is not a fence at all — the same "malformed half-pair is no
175
+ * anchor" rule `anchored-region.ts` applies.
176
+ *
177
+ * **First pair wins, same as `anchoredRegionBounds` — a SECOND pair in the
178
+ * same file is not masked.** By design there is exactly one fenced home in
179
+ * the whole doctrine tree (the Goal this check exists to hold), so a
180
+ * second pair anywhere is itself a doctrine defect, not a shape this
181
+ * function needs to accommodate; scanning per-file rather than per-pair
182
+ * keeps that failure visible (a second, unmasked pair still reports its
183
+ * own `vendor-name` findings) instead of silently exempting it too.
184
+ */
185
+ function maskVendorExampleRegion(masked: string): string {
186
+ const start = VENDOR_EXAMPLE_START.exec(masked)
187
+ if (!start) return masked
188
+ const afterStart = start.index + start[0].length
189
+ const end = VENDOR_EXAMPLE_END.exec(masked.slice(afterStart))
190
+ if (!end) return masked
191
+ const regionEnd = afterStart + end.index + end[0].length
192
+ const region = masked.slice(start.index, regionEnd)
193
+ const blanked = region.replace(/[^\n]/g, ' ')
194
+ return masked.slice(0, start.index) + blanked + masked.slice(regionEnd)
195
+ }
196
+
197
+ /**
198
+ * Every vendor-name-list match in `content`, code-blind (`maskCode`, the
199
+ * same grammar `anchored-region.ts` uses) and blind to the one fenced
200
+ * `AEG:VENDOR-EXAMPLE` home. A vendor word that is only part of an
201
+ * already-portable inline-code path citation (`` `.claude/hooks/x.sh` ``,
202
+ * `` `CLAUDE.md` ``) never reaches this scan at all — it is masked before
203
+ * the word list ever runs, the same way a fenced worked example is blind to
204
+ * `body-bare-digits`.
205
+ */
206
+ function extractVendorMentions(content: string): { name: string; index: number }[] {
207
+ const scoped = maskVendorExampleRegion(maskCode(content))
208
+ const found: { name: string; index: number }[] = []
209
+ const pattern = new RegExp(VENDOR_NAME_SOURCE, 'gi')
210
+ let match: RegExpExecArray | null = pattern.exec(scoped)
211
+ while (match !== null) {
212
+ found.push({ name: match[0], index: match.index })
213
+ match = pattern.exec(scoped)
214
+ }
215
+ return found
216
+ }
217
+
108
218
  /** Every inline-backtick span in `content`, tested against `CITED_PATH_PATTERN`. */
109
219
  function extractCitedPaths(content: string): { cited: string; index: number }[] {
110
220
  const found: { cited: string; index: number }[] = []
@@ -140,10 +250,12 @@ function isPortable(cited: string, shipsPrefix: string): boolean {
140
250
  }
141
251
 
142
252
  /**
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.
253
+ * Sweeps every file under `shipsPrefix` for two independent finding kinds:
254
+ * a cited path whose top segment is not allow-listed portable (`'path'`),
255
+ * and a vendor-name-list word used in prose outside the one fenced
256
+ * `AEG:VENDOR-EXAMPLE` home (`'vendor-name'`, Issue #298). Files outside
257
+ * `shipsPrefix` are out of scope entirely — this check only judges what the
258
+ * shipped doctrine tree itself cites/names, never a repo's other source.
147
259
  */
148
260
  export function checkDoctrinePortability(
149
261
  files: readonly PortabilitySourceFile[],
@@ -161,7 +273,18 @@ export function checkDoctrinePortability(
161
273
  file: file.path,
162
274
  line: lineAtIndex(file.content, index),
163
275
  cited,
164
- message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`
276
+ message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`,
277
+ kind: 'path'
278
+ })
279
+ }
280
+
281
+ for (const { name, index } of extractVendorMentions(file.content)) {
282
+ findings.push({
283
+ file: file.path,
284
+ line: lineAtIndex(file.content, index),
285
+ cited: name,
286
+ message: `names "${name}" outside the one fenced vendor-example home (an <!-- AEG:VENDOR-EXAMPLE:START --> … <!-- AEG:VENDOR-EXAMPLE:END --> pair) — portable doctrine refers to a host generically everywhere else`,
287
+ kind: 'vendor-name'
165
288
  })
166
289
  }
167
290
  }
@@ -121,6 +121,7 @@ export const SHIPPED_BIN_AUDIENCE: Record<string, GateAudience> = {}
121
121
  */
122
122
  export const CLI_CHECK_RING: Readonly<Record<string, 0 | 1 | 2>> = {
123
123
  'brief-shape': 0,
124
+ 'pr-report-density': 0,
124
125
  'doc-coverage': 0,
125
126
  coherence: 0,
126
127
  'dispatch-readiness': 0,
@@ -139,8 +140,10 @@ export const CLI_CHECK_RING: Readonly<Record<string, 0 | 1 | 2>> = {
139
140
  'evidence-fresh': 1,
140
141
  'reader-resolvable-prose': 0,
141
142
  'retired-vocabulary': 0,
143
+ 'doctrine-no-procedures': 0,
142
144
  'doctrine-portability': 0,
143
145
  'workspace-escape': 0,
146
+ 'exec-bits': 0,
144
147
  'changeset-coverage': 0,
145
148
  'quoted-command': 0,
146
149
  'main-branch-refusal': 0,
@@ -10,14 +10,19 @@ export {
10
10
  CROSS_CUTTING_CANDIDATES,
11
11
  deriveBuiltinCrossCuttingDefaults,
12
12
  deriveWorkspacePackageDomains,
13
- parsePnpmWorkspaceYaml
13
+ parsePnpmWorkspaceYaml,
14
+ resolveWorkspaceEntry
14
15
  } from './blast-radius-domains'
16
+ export { buildConsumersOf, deriveWorkspaceMemberDirs } from './consumer-enumeration'
17
+ export type { PackageManifest } from './consumer-enumeration'
15
18
  export { parseTranche } from './parse-tranche'
16
19
  export { deriveTranche } from './derive-tranche'
17
20
  export { parseLedger, rowFromCells } from './parse-ledger'
18
21
  export { sumLedger } from './sum-ledger'
19
22
  export { aggregateTaskTokenRows, parseTokenReportEntries, parseTokensLines } from './parse-token-report'
20
23
  export type { TokenSourcePr } from './parse-token-report'
24
+ export { deriveReviewStatus, parseDeveloperRoundMarker, renderReviewStatus } from './review-status'
25
+ export type { ReviewStatus, ReviewStatusInput } from './review-status'
21
26
  export { formatBreakdown, formatTokenReportRow, formatTokensLine } from './report-tokens'
22
27
  export type { TokenReportRowInput, TokensLineInput, TranscriptSummary, UsageComponents } from './report-tokens'
23
28
  // The Claude Code collection adapter (`tranche-model.md` §12 layer 2) — one
@@ -72,9 +77,14 @@ export {
72
77
  checkAutonomyClause,
73
78
  checkBriefSections,
74
79
  checkClosesN as checkBriefClosesN,
80
+ checkCommandsCarryOutput,
81
+ checkConsumerTests,
82
+ checkDefeatCases,
75
83
  checkDocUpdateList,
76
84
  checkForField,
77
85
  checkForgeTitle,
86
+ checkNoAgentBoxes,
87
+ checkNoUnpinnedCodeClaims,
78
88
  checkPlanPrNoCloses,
79
89
  checkPremiseCoverage,
80
90
  checkPrincipalPlaceholder,
@@ -85,14 +95,24 @@ export {
85
95
  checkTestPlanExclusivity,
86
96
  checkTierField,
87
97
  checkWorktreeStep0,
98
+ AGENT_BOXES_REFUSED_SINCE_PR,
99
+ BRIEF_RULES_SINCE_PR,
100
+ COMMAND_WORDS,
88
101
  COMMIT_TYPE_STYLE,
89
102
  COMMIT_TYPES,
103
+ extractFencedBlocks,
90
104
  headerRegion,
91
105
  inferBranchFromBody,
92
106
  isBriefShaped,
93
- isTaskBranch
107
+ isGrandfatherableBriefRuleError,
108
+ isTaskBranch,
109
+ partitionBriefErrorsByRollout
94
110
  } from './brief-validation'
95
- export type { BriefSectionResult, BriefSectionsOptions } from './brief-validation'
111
+ export type { BriefSectionResult, BriefSectionsOptions, FencedBlock } from './brief-validation'
112
+ export { checkDoctrineNoProcedures } from './doctrine-no-procedures'
113
+ export type { DoctrineFile, DoctrineProcedureFinding } from './doctrine-no-procedures'
114
+ export { checkPrReportDensity, checkScopeDensity, checkSummaryDensity } from './pr-report-density'
115
+ export type { DensityResult } from './pr-report-density'
96
116
  export {
97
117
  checkA1,
98
118
  checkA2,
@@ -299,3 +319,5 @@ export type {
299
319
  } from './milestone-validation'
300
320
  export { findWorkspaceEscapes } from './workspace-escape'
301
321
  export type { WorkspaceEscapeFinding, WorkspaceEscapeReason, WorkspaceEscapeSourceFile } from './workspace-escape'
322
+ export { parseRationaleFields, renderBrief } from './brief-render'
323
+ export type { BriefFacts, RationaleFieldKey, RenderResult, SurfaceFileFact } from './brief-render'
@@ -1,6 +1,7 @@
1
1
  import { rowFromCells } from './parse-ledger'
2
2
  import { splitTableRow } from './parse-registry'
3
3
  import type { LedgerRow } from './types'
4
+ import { isPrincipal } from './waiver-label'
4
5
 
5
6
  /**
6
7
  * Live-source token parsing (aeg-forge-state-v1 task 4b, #445). Where
@@ -138,28 +139,39 @@ function isDashOnly(s: string): boolean {
138
139
  return s === '—' || s === '-' || s === '–'
139
140
  }
140
141
 
141
- /** One merged PR's body + top-level comments — the caller's fetch result, no I/O here. */
142
+ /**
143
+ * One merged PR's body + top-level comments — the caller's fetch result, no
144
+ * I/O here. Each comment carries its author's login (`null` when the forge
145
+ * reports none, e.g. a deleted account) because a comment's `Tokens:` line is
146
+ * only a ledger row when an allowlisted principal posted it — every agent in
147
+ * this model posts under the Principal's own `gh` identity, so an unfiltered
148
+ * read counts a stranger's pasted table as a real turn.
149
+ */
142
150
  export type TokenSourcePr = {
143
151
  number: number
144
152
  body: string
145
- comments: string[]
153
+ comments: { body: string; author: string | null }[]
146
154
  }
147
155
 
148
156
  /**
149
157
  * Pure aggregation over every merged PR associated with a task (its own
150
158
  * branch's PR(s), plus any plan/cross-referenced PR carrying the Planner's
151
159
  * report) — Developer "Token report" entries and `Tokens: …` lines from
152
- * each PR body, and `Tokens: …` lines from each PR's comments. Missing or
160
+ * each PR body, and `Tokens: …` lines from each PR's comments the latter
161
+ * only from a comment an allowlisted principal authored. Missing or
153
162
  * malformed reports simply produce no row for that report — never a
154
163
  * fabricated figure — matching `archivist.md`'s own "flag it under DANGLING
155
164
  * instead" discipline; the caller decides how to surface an empty result.
156
165
  */
157
- export function aggregateTaskTokenRows(prs: TokenSourcePr[]): LedgerRow[] {
166
+ export function aggregateTaskTokenRows(prs: TokenSourcePr[], principalAllowlist: string[]): LedgerRow[] {
158
167
  const out: LedgerRow[] = []
159
168
  for (const pr of prs) {
160
169
  out.push(...parseTokenReportEntries(pr.body))
161
170
  out.push(...parseTokensLines(pr.body))
162
- for (const comment of pr.comments) out.push(...parseTokensLines(comment))
171
+ for (const comment of pr.comments) {
172
+ if (!isPrincipal(comment.author, principalAllowlist)) continue
173
+ out.push(...parseTokensLines(comment.body))
174
+ }
163
175
  }
164
176
  return out
165
177
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * PR-report density (aeg-governance-hardening, dogfooding finding 2026-09-02).
3
+ * Pure — no `fs`, no `fetch`, no `process.env`.
4
+ *
5
+ * `aeg-root/roles/developer.md` § "PR body — canonical form" and
6
+ * `aeg-root/templates/pr-report-template.md` both require `## Summary` and
7
+ * `## Scope` to be exactly one paragraph — narrative root-cause detail
8
+ * belongs in a section of its own below the canonical four, a commit
9
+ * message, or the changeset, never padded into the report itself and never
10
+ * hand-typed into the AEG:EVIDENCE block. Nothing enforced that rule
11
+ * mechanically:
12
+ * confirmed live, three real PRs (`atta-labs/vinaya#343`, `#346`, `#354`)
13
+ * shipped multi-paragraph Summary/Scope sections and nothing caught it,
14
+ * neither at `vinaya pr create`/`pr edit` time nor in CI. Longer, denser
15
+ * prose is also strictly more surface for every other body-text gate
16
+ * (`body-bare-digits`, `reader-resolvable-prose`'s glossary rule) — the same
17
+ * class of gap `#341` hit in code, just in prose instead.
18
+ *
19
+ * Deterministic and structural only, matching `brief-validation.ts`'s own
20
+ * "presence-only" philosophy: this counts blank-line-delimited text blocks,
21
+ * never judges whether the prose itself is good.
22
+ *
23
+ * The rule is literal — one paragraph, full stop — so a `### subsection`, a
24
+ * bullet list, or a markdown table under Summary/Scope all fail too, same as
25
+ * a second prose paragraph would (confirmed by probe, PR review on #358).
26
+ * That is intentional, not an oversight: `developer.md`'s own escape hatch
27
+ * for exactly this case is "Add anything you want beneath the four
28
+ * sections" — structured detail belongs in a section of its own below
29
+ * Summary/Scope/Test-plan/Evidence, not folded into one of the four. A
30
+ * multi-line blockquote is the one shape this rule does NOT split on (its
31
+ * `\n>\n` continuation lines are never blank) — but only when the
32
+ * blockquote IS the section, start to finish: prose before or after it is
33
+ * still a separate blank-line-delimited block, so "intro sentence, then a
34
+ * blockquote aside, then a closing sentence" is three blocks and still
35
+ * fails (confirmed by probe, PR review on #358 — this is not the
36
+ * "structured aside alongside prose" shape it can look like at a glance).
37
+ * The exemption only covers a section whose entire content, quoted in
38
+ * full, is the one thing being said.
39
+ */
40
+
41
+ import { anchoredRegionBounds, stripCode } from './anchored-region'
42
+
43
+ export type DensityResult = { status: 'pass' | 'fail'; errors: string[] }
44
+
45
+ /**
46
+ * The named `## <heading>` section's raw body text — from just after the
47
+ * heading line to the next `##`-or-shallower heading, or end of body.
48
+ * `null` when the heading itself is absent (a different check's job to
49
+ * require it — this one is silent on presence, per the module's scope).
50
+ */
51
+ function headingSectionBody(prBody: string, heading: string): string | null {
52
+ const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, 'im')
53
+ const m = headingRe.exec(prBody)
54
+ if (!m) return null
55
+ const start = m.index + m[0].length
56
+ const rest = prBody.slice(start)
57
+ const nextHeading = /^#{1,2}\s/m.exec(rest)
58
+ return nextHeading ? rest.slice(0, nextHeading.index) : rest
59
+ }
60
+
61
+ /**
62
+ * `headingSectionBody`'s text with every well-formed `AEG:*` anchor block removed
63
+ * outright (not just unwrapped) — an anchored field (Scope's trailing
64
+ * `AEG:TIER` block, in the canonical template) is a field, not a second
65
+ * prose paragraph, and must not count as one. Reuses `anchoredRegionBounds`
66
+ * (the same span `vinaya pr report --write` targets) rather than a second,
67
+ * drifting regex for "what an anchor block looks like."
68
+ */
69
+ function stripAnchorBlocks(sectionText: string): string {
70
+ let result = sectionText
71
+ for (const field of ['CLOSES', 'PROJECT', 'TIER', 'PREMISE', 'TEST-PLAN', 'EVIDENCE'] as const) {
72
+ for (;;) {
73
+ const bounds = anchoredRegionBounds(result, field)
74
+ if (!bounds) break
75
+ result = result.slice(0, bounds.outerStart) + result.slice(bounds.outerEnd)
76
+ }
77
+ }
78
+ return result
79
+ }
80
+
81
+ /** Non-empty, blank-line-delimited text blocks, code-blind (a fenced example inside the section is not a second paragraph). */
82
+ function paragraphCount(sectionText: string): number {
83
+ const stripped = stripCode(stripAnchorBlocks(sectionText)).trim()
84
+ if (stripped.length === 0) return 0
85
+ return stripped
86
+ .split(/\n[ \t]*\n/)
87
+ .map((block) => block.trim())
88
+ .filter((block) => block.length > 0).length
89
+ }
90
+
91
+ function checkSectionDensity(prBody: string, heading: string): DensityResult {
92
+ const body = headingSectionBody(prBody, heading)
93
+ if (body === null) return { status: 'pass', errors: [] }
94
+ const count = paragraphCount(body)
95
+ if (count <= 1) return { status: 'pass', errors: [] }
96
+ return {
97
+ status: 'fail',
98
+ errors: [
99
+ `pr-report-density ${heading}: "## ${heading}" holds ${count} paragraphs — the canonical PR-report form (aeg-root/roles/developer.md § PR body) requires exactly one. Collapse it to one paragraph; move the rest into a section of your own below the canonical four ("Add anything you want beneath the four sections", developer.md), a commit message, or the changeset — never into the AEG:EVIDENCE block, which is emitted only, never hand-typed.`
100
+ ]
101
+ }
102
+ }
103
+
104
+ export function checkSummaryDensity(prBody: string): DensityResult {
105
+ return checkSectionDensity(prBody, 'Summary')
106
+ }
107
+
108
+ export function checkScopeDensity(prBody: string): DensityResult {
109
+ return checkSectionDensity(prBody, 'Scope')
110
+ }
111
+
112
+ /** Aggregates both section checks — one error line per over-dense section. */
113
+ export function checkPrReportDensity(prBody: string): { errors: string[] } {
114
+ const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)]
115
+ return { errors: results.flatMap((r) => r.errors) }
116
+ }
@@ -80,6 +80,41 @@ export function parsePremiseBlock(prBody: string): PremiseAssertion[] {
80
80
  return assertions
81
81
  }
82
82
 
83
+ /**
84
+ * The `Premise:` block's own raw text (header line through its last bullet),
85
+ * mirroring `parsePremiseBlock`'s exact same header/bullet/stop rules so the
86
+ * two can never disagree about where the block ends. Returns `null` when no
87
+ * header is found. Used by `brief-validation.ts`'s `checkNoUnpinnedCodeClaims`
88
+ * to exclude the block's own bullets (a pin's path is a legitimate file
89
+ * reference, not an unpinned prose claim) — reuses this parser's block
90
+ * boundaries rather than a second one.
91
+ */
92
+ export function premiseBlockText(prBody: string): string | null {
93
+ const searchIn = anchoredRegion(prBody, 'PREMISE') ?? prBody
94
+ const lines = searchIn.split(/\r?\n/)
95
+ let startIdx = -1
96
+ let endIdxExclusive = lines.length
97
+
98
+ for (let i = 0; i < lines.length; i++) {
99
+ const line = (lines[i] as string).trim()
100
+ if (startIdx === -1) {
101
+ if (isPremiseHeader(line)) startIdx = i
102
+ continue
103
+ }
104
+ if (line === '') {
105
+ endIdxExclusive = i
106
+ break
107
+ }
108
+ if (!PREMISE_LINE.test(line)) {
109
+ endIdxExclusive = i
110
+ break
111
+ }
112
+ }
113
+
114
+ if (startIdx === -1) return null
115
+ return lines.slice(startIdx, endIdxExclusive).join('\n')
116
+ }
117
+
83
118
  export type PremiseCheckResult = { pass: boolean; failures: string[] }
84
119
 
85
120
  /**
@@ -75,8 +75,8 @@ import { classifyProseFile, type ProseFileClass } from './reader-resolvable-pros
75
75
  // package's own barrel comment ("reachable through the `./strip-code`
76
76
  // subpath for the two callers that need them"). Not through
77
77
  // `anchored-region.ts`'s re-export: that file's own `anchoredRegionBounds`
78
- // is scoped to its closed six-field `AnchorField` grammar, which this
79
- // marker (a free-form cited-file path, not one of those six names) cannot
78
+ // is scoped to its closed seven-field `AnchorField` grammar, which this
79
+ // marker (a free-form cited-file path, not one of those seven names) cannot
80
80
  // use without widening a PR-body-specific type for a doc-authoring concern
81
81
  // outside this task's surface. Reusing `maskCode` itself — rather than
82
82
  // writing a second masker — is the actual "do not write a second anchor
@@ -164,7 +164,7 @@ function extractQuotedText(raw: string): string {
164
164
  * (index-preserving, same primitive `anchored-region.ts` uses) to find
165
165
  * marker positions code-blind, then slices the real inner text from the
166
166
  * original `content` at those same indices, exactly as `anchoredRegionBounds`
167
- * does for its own six fields.
167
+ * does for its own seven fields.
168
168
  */
169
169
  function findQuotesInFile(path: string, content: string): CitedQuote[] {
170
170
  const masked = maskCode(content)