@attalabs/vinaya 0.21.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.
- package/README.md +15 -4
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/brief-developer.md +1 -0
- package/aeg-root/contracts/developer-reviewer.md +3 -3
- package/aeg-root/contracts/reviewer-archivist.md +3 -3
- package/aeg-root/enforcement.md +28 -21
- package/aeg-root/process.md +8 -4
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/brief-author.md +9 -3
- package/aeg-root/roles/developer.md +63 -31
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/principal.md +14 -0
- package/aeg-root/roles/reviewer.md +32 -15
- package/aeg-root/roles/security.md +15 -5
- package/aeg-root/skills/aeg/SKILL.md +9 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
- package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
- package/aeg-root/state-machine.md +9 -11
- package/aeg-root/templates/brief-template.md +6 -6
- package/aeg-root/templates/pr-report-template.md +12 -7
- package/aeg-root/tranche-model.md +9 -5
- package/dist/checks/bin/check-body-bare-digits.js +667 -148
- package/dist/checks/bin/check-branch-topology.js +714 -149
- package/dist/checks/bin/check-brief-shape.js +704 -147
- package/dist/checks/bin/check-changeset-coverage.js +1046 -166
- package/dist/checks/bin/check-closes-n.js +714 -149
- package/dist/checks/bin/check-coherence.js +729 -156
- package/dist/checks/bin/check-dead-branch-push.js +663 -144
- package/dist/checks/bin/check-dispatch-readiness.js +728 -155
- package/dist/checks/bin/check-doc-coverage-push.js +838 -261
- package/dist/checks/bin/check-doc-coverage.js +838 -261
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +799 -155
- package/dist/checks/bin/check-evidence-fresh.js +1132 -206
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +716 -151
- package/dist/checks/bin/check-issue-assignment.js +714 -149
- package/dist/checks/bin/check-main-branch-refusal.js +663 -144
- package/dist/checks/bin/check-no-disk-state.js +663 -144
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +741 -166
- package/dist/checks/bin/check-reader-resolvable-prose.js +768 -173
- package/dist/checks/bin/check-registry-gates.js +726 -154
- package/dist/checks/bin/check-retired-vocabulary.js +760 -165
- package/dist/checks/bin/check-review-gate.js +783 -148
- package/dist/checks/bin/check-single-plan-pr.js +663 -144
- package/dist/checks/bin/check-test-plan.js +664 -145
- package/dist/checks/bin/check-token-collection-wired.js +665 -146
- package/dist/checks/bin/check-token-report.js +4509 -0
- package/dist/checks/bin/check-workspace-escape.js +1129 -183
- package/dist/index.js +8458 -5403
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
- 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
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__02c71kv._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0ozbaq0._.js → [root-of-the-server]__1mcitst._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1mrtj0f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_06it6s-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_brutal_components_index_ts_0n_lcc4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_0wvpm_z._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_1_cfi48._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/00pi8vhg8w6gi.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0r4i_ks9z02bw.js → 03q0q9n6-xah8.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2_k6j1o9q5nn4.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0vc2u252q5q6o.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1k916ih3p4nsg.js → 13g16cu00g570.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2hmannd4bjhzy.js → 1sfxfunirkcr6.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1t2kft7bxzuxn.js → 206ajg8f1a-_h.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hfhxjy577l5d.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{25kbt5k-xb2a5.js → 279qq4twinc97.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{112g08xpx3kro.js → 2ka8dg687auhj.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2ou_81ahqw31e.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0up5x3jg-cwov.js → 2rn89uwtf2lwc.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3mqu_uhluy19i.css → 3qc67qhcvbg0o.css} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1yqs2zgvwa4p1.js → 3zugx9esh2x51.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0_jnwbarv-cqk.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1ybrihl5_aocy.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3r5efmleneaco.js +0 -1
- /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
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brief→Developer brief-validation grammar (aeg-governance-hardening
|
|
3
|
+
* task 2). Pure — no `fs`, no `fetch`, no `process.env`. The CLI shim
|
|
4
|
+
* (`bin/verify-brief.ts`) reads `PR_BODY`, then
|
|
5
|
+
* calls `checkBriefSections`.
|
|
6
|
+
*
|
|
7
|
+
* Scope is presence-only (per the Planner's trap): this gate cannot judge
|
|
8
|
+
* whether a Test Plan item is truly scriptable, whether `unit-tests-only` is
|
|
9
|
+
* justified by the surface map, or whether a doc-update entry is factually
|
|
10
|
+
* correct — those remain Reviewer + Verification judgment. It only confirms each
|
|
11
|
+
* required brief section exists in the PR body, in the shape the
|
|
12
|
+
* `brief-authoring` skill and `brief-developer` contract define.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { type AnchorField, anchoredRegion, stripCode } from './anchored-region'
|
|
16
|
+
import { parsePremiseBlock } from './premise-check'
|
|
17
|
+
import { locateTestPlanSection } from './test-plan-section'
|
|
18
|
+
|
|
19
|
+
export type BriefSectionResult = { status: 'pass' | 'fail'; errors: string[] }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The PR body's header block: everything before the first h2+ heading. The
|
|
23
|
+
* canonical PR-body form (`roles/developer.md`) puts the metadata fields
|
|
24
|
+
* (Tier, For, Project, Closes) at the top, before `## Summary`.
|
|
25
|
+
* Anchoring field extraction here — shared with `archive-task.ts` — is what
|
|
26
|
+
* stops prose in later sections that merely *mentions* a field name (e.g. a
|
|
27
|
+
* "Decisions made" paragraph discussing the `Ticket:` field) from being
|
|
28
|
+
* parsed as the field itself. Regression from #311's first live archivist
|
|
29
|
+
* run, where exactly that happened.
|
|
30
|
+
*/
|
|
31
|
+
export function headerRegion(prBody: string): string {
|
|
32
|
+
const m = prBody.match(/^##\s/m)
|
|
33
|
+
return m?.index !== undefined ? prBody.slice(0, m.index) : prBody
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Tolerant header-field reader: accepts `Field: value` and `**Field:** value`,
|
|
38
|
+
* line-anchored, searched only in the header region. Stops at line end or a
|
|
39
|
+
* `·` metadata separator.
|
|
40
|
+
*
|
|
41
|
+
* When `anchor` is given and the body carries that anchor pair
|
|
42
|
+
* (`anchored-region.ts`, task 30), the pair replaces the header region as the
|
|
43
|
+
* one place the field is read from — a field-shaped line anywhere else in the
|
|
44
|
+
* body is ignored. Bodies without the pair parse exactly as before.
|
|
45
|
+
*/
|
|
46
|
+
function headerField(prBody: string, labelPattern: string, anchor?: AnchorField): string | null {
|
|
47
|
+
const anchored = anchor !== undefined ? anchoredRegion(prBody, anchor) : null
|
|
48
|
+
const region = anchored ?? headerRegion(prBody)
|
|
49
|
+
const re = new RegExp(`^(?:\\*\\*)?\\s*${labelPattern}\\s*(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([^\\n·]+)`, 'im')
|
|
50
|
+
const m = region.match(re)
|
|
51
|
+
if (!m) return null
|
|
52
|
+
const value = (m[1] as string).trim()
|
|
53
|
+
return value.length > 0 ? value : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Strips markdown emphasis markers and collapses whitespace, for tolerant phrase matching. */
|
|
57
|
+
function normalize(text: string): string {
|
|
58
|
+
return text.replace(/[*_]/g, '').replace(/\s+/g, ' ')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function headingCheck(prBody: string, keywordPattern: string, sectionName: string): BriefSectionResult {
|
|
62
|
+
const re = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?(?:\\d+[a-z]?\\.\\s*)?[^\\n]*${keywordPattern}`, 'im')
|
|
63
|
+
if (re.test(prBody)) return { status: 'pass', errors: [] }
|
|
64
|
+
return {
|
|
65
|
+
status: 'fail',
|
|
66
|
+
errors: [`brief-validation ${sectionName}: no "${sectionName}" section found in the PR body.`]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Tier field — reuses the canonical `@attalabs/aeg-core` parser (do not re-implement). */
|
|
71
|
+
export function checkTierField(prBody: string, readTier: (body: string) => 0 | 1 | 3 | null): BriefSectionResult {
|
|
72
|
+
if (readTier(prBody) !== null) return { status: 'pass', errors: [] }
|
|
73
|
+
return {
|
|
74
|
+
status: 'fail',
|
|
75
|
+
errors: [
|
|
76
|
+
'brief-validation tier: no `Tier:` field found in the PR body (expected `Tier: 0|1|3` or `**Tier:** 0|1|3`).'
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The `unit-tests-only` sentinel, tolerating the markdown emphasis every other
|
|
83
|
+
* brief field carries (`**Tier:**`, `**Project:**`, …). All three of
|
|
84
|
+
* `Test Plan: unit-tests-only`, `**Test Plan:** unit-tests-only` and
|
|
85
|
+
* `**Test Plan**: unit-tests-only` match.
|
|
86
|
+
*
|
|
87
|
+
* The bold forms used to fail: the pattern was `/Test Plan\s*:\s*unit-tests-only/i`,
|
|
88
|
+
* and `\s*` cannot cross the `**` sitting between the colon and the value — so
|
|
89
|
+
* `brief-authoring/SKILL.md` §9's own canonical example (`**Test Plan:** unit-tests-only`)
|
|
90
|
+
* was rejected by the gate that documents it, and any brief copying the skill
|
|
91
|
+
* verbatim bounced. Two checks read this sentinel and both were wrong in
|
|
92
|
+
* different directions: `checkTestPlan` false-FAILED a well-formed brief, while
|
|
93
|
+
* `checkTestPlanExclusivity` silently false-PASSED a self-contradictory one (a
|
|
94
|
+
* bolded `unit-tests-only` **plus** tagged checkboxes never tripped it, because
|
|
95
|
+
* its own guard clause never matched either) — the exact combination that guard
|
|
96
|
+
* was built for in #340. Fixing the doc instead of the pattern was rejected:
|
|
97
|
+
* bold is the house convention for brief fields, so an unbolded Test Plan is the
|
|
98
|
+
* odd one out and drifts back the moment someone tidies it.
|
|
99
|
+
*/
|
|
100
|
+
const TEST_PLAN_UNIT_TESTS_ONLY_RE = /(?:\*\*)?Test Plan(?:\*\*)?\s*:\s*(?:\*\*)?\s*unit-tests-only/i
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The Test Plan section text these three checks scan — never the raw body.
|
|
104
|
+
* Delegates to `locateTestPlanSection` (the same locator `test-plan.ts`'s
|
|
105
|
+
* merge-gate check uses) so an `AEG:TEST-PLAN` anchor pair is authoritative
|
|
106
|
+
* here exactly as it already is there. Without this, a body that anchors its
|
|
107
|
+
* real Test Plan AND carries a pasted verbatim reference copy of the brief
|
|
108
|
+
* (the template's own instruction) has its checkbox lines double-counted:
|
|
109
|
+
* `checkTestPlanExclusivity`/`checkPrincipalPlaceholder` scanned the whole
|
|
110
|
+
* body and fired on checkboxes living only in the quoted reference copy,
|
|
111
|
+
* disagreeing with `test-plan.ts` about which bytes are the real field
|
|
112
|
+
* (found live: a well-formed anchored brief failed `brief-shape` while
|
|
113
|
+
* `test-plan` passed the same body). Falls back to the full body when no
|
|
114
|
+
* section is locatable, matching every other anchor consumer's "anchors are
|
|
115
|
+
* additive, never required" contract.
|
|
116
|
+
*/
|
|
117
|
+
function testPlanRegion(prBody: string): string {
|
|
118
|
+
const located = locateTestPlanSection(prBody)
|
|
119
|
+
return located.found ? located.section : prBody
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Test Plan — pass iff the body contains the `Test Plan: unit-tests-only`
|
|
124
|
+
* sentinel (bolded or not — see `TEST_PLAN_UNIT_TESTS_ONLY_RE`), OR at least one
|
|
125
|
+
* `**[agent]**`/`**[principal]**`-tagged checklist line. Presence-only: does not
|
|
126
|
+
* judge whether tags are correctly assigned or whether `unit-tests-only` is
|
|
127
|
+
* justified by the surface map.
|
|
128
|
+
*/
|
|
129
|
+
export function checkTestPlan(prBody: string): BriefSectionResult {
|
|
130
|
+
const region = testPlanRegion(prBody)
|
|
131
|
+
if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region)) return { status: 'pass', errors: [] }
|
|
132
|
+
if (/\*\*\[(?:agent|principal)\]\*\*/.test(region)) return { status: 'pass', errors: [] }
|
|
133
|
+
return {
|
|
134
|
+
status: 'fail',
|
|
135
|
+
errors: [
|
|
136
|
+
'brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item.'
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Test Plan shape guard (aeg-governance-hardening task 20 follow-up, #340) —
|
|
143
|
+
* `Test Plan: unit-tests-only` and a tagged checkbox item are mutually
|
|
144
|
+
* exclusive per brief-authoring §9 ("The two fields are coupled; Brief
|
|
145
|
+
* Validation cross-checks them"): `unit-tests-only` declares there is no
|
|
146
|
+
* checklist because there is nothing runtime to check off, so a body
|
|
147
|
+
* carrying both is self-contradictory. Regression source: PR #363's own
|
|
148
|
+
* original brief declared `unit-tests-only` while its Test Plan also listed
|
|
149
|
+
* `- [x]`/`- [ ]` `[agent]`/`[principal]` items — a combination this gate
|
|
150
|
+
* previously let through.
|
|
151
|
+
*/
|
|
152
|
+
export function checkTestPlanExclusivity(prBody: string): BriefSectionResult {
|
|
153
|
+
const region = testPlanRegion(prBody)
|
|
154
|
+
if (!TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region)) return { status: 'pass', errors: [] }
|
|
155
|
+
if (/^-\s*\[[ xX]\]\s*\*{2}\[(?:agent|principal)\]\*{2}/im.test(region)) {
|
|
156
|
+
return {
|
|
157
|
+
status: 'fail',
|
|
158
|
+
errors: [
|
|
159
|
+
'brief-validation Test Plan shape: `Test Plan: unit-tests-only` and a `- [ ]`/`- [x]` tagged checkbox item are mutually exclusive (brief-authoring §9) — declare one form, not both.'
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { status: 'pass', errors: [] }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Principal-placeholder guard (aeg-governance-hardening task 20 follow-up,
|
|
168
|
+
* #340) — a `**[principal]**` checkbox item whose content is a
|
|
169
|
+
* none-placeholder ("None — …") is untickable by construction: nobody can
|
|
170
|
+
* check a box asserting there is nothing to verify, so it blocks the merge
|
|
171
|
+
* gate forever. If a brief genuinely has no principal-runnable surface, the
|
|
172
|
+
* item must be omitted entirely, not declared and left permanently unticked.
|
|
173
|
+
*/
|
|
174
|
+
export function checkPrincipalPlaceholder(prBody: string): BriefSectionResult {
|
|
175
|
+
const region = testPlanRegion(prBody)
|
|
176
|
+
const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim
|
|
177
|
+
for (const m of region.matchAll(lineRe)) {
|
|
178
|
+
const content = m[1] ?? ''
|
|
179
|
+
if (/^\s*None\b/i.test(content)) {
|
|
180
|
+
return {
|
|
181
|
+
status: 'fail',
|
|
182
|
+
errors: [
|
|
183
|
+
'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { status: 'pass', errors: [] }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Premise coverage (this task, aeg-governance-hardening 11, #324) — pass iff
|
|
193
|
+
* either (a) at least one `Premise:` assertion's path matches a file in
|
|
194
|
+
* `surfaceFiles` (the §4 surface map's file list — in practice, the PR's
|
|
195
|
+
* actual changed-file list, the same diff-derived-truth philosophy
|
|
196
|
+
* `deriveTierFromDiff` already uses), or (b) `surfaceFiles` is empty (a
|
|
197
|
+
* Tier 0 brief with zero runtime/code surface has nothing to pin — mirrors
|
|
198
|
+
* `checkTestPlan`'s `unit-tests-only` exemption). Presence-only: does not
|
|
199
|
+
* judge whether the pinned premise is the *right* one to have pinned.
|
|
200
|
+
*/
|
|
201
|
+
export function checkPremiseCoverage(prBody: string, surfaceFiles: string[]): BriefSectionResult {
|
|
202
|
+
if (surfaceFiles.length === 0) return { status: 'pass', errors: [] }
|
|
203
|
+
const assertions = parsePremiseBlock(prBody)
|
|
204
|
+
if (assertions.some((a) => surfaceFiles.includes(a.path))) return { status: 'pass', errors: [] }
|
|
205
|
+
return {
|
|
206
|
+
status: 'fail',
|
|
207
|
+
errors: [
|
|
208
|
+
'brief-validation Premise: no `Premise:` assertion found whose path matches a file in the surface map — a brief with a real code surface must pin at least one premise (aeg-governance-hardening task 11).'
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function checkSurfaceMap(prBody: string): BriefSectionResult {
|
|
214
|
+
return headingCheck(prBody, '(?:technical\\s+)?surface map', 'Technical surface map')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function checkDocUpdateList(prBody: string): BriefSectionResult {
|
|
218
|
+
return headingCheck(prBody, '(?:documentation|doc)[- ]update(?:\\s+list)?', 'Documentation-update list')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function checkWorktreeStep0(prBody: string): BriefSectionResult {
|
|
222
|
+
if (/git worktree add/.test(prBody)) return { status: 'pass', errors: [] }
|
|
223
|
+
return {
|
|
224
|
+
status: 'fail',
|
|
225
|
+
errors: ['brief-validation worktree Step 0: no `git worktree add` command found in the PR body.']
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function checkStopConditions(prBody: string): BriefSectionResult {
|
|
230
|
+
return headingCheck(prBody, 'stop conditions', 'Stop conditions')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Autonomy clause — matches the standing clause's core phrase, case-insensitively,
|
|
235
|
+
* tolerant of whitespace and emphasis markup (`**Autonomy:**` vs `Autonomy:`).
|
|
236
|
+
* Per the Planner's trap, this is deliberately verbatim-ish rather than a loose
|
|
237
|
+
* "mentions autonomy somewhere" match — the phrase is the actual invariant.
|
|
238
|
+
*/
|
|
239
|
+
export function checkAutonomyClause(prBody: string): BriefSectionResult {
|
|
240
|
+
const normalized = normalize(prBody).toLowerCase()
|
|
241
|
+
if (/do not stop to ask clarifying questions/.test(normalized)) return { status: 'pass', errors: [] }
|
|
242
|
+
return {
|
|
243
|
+
status: 'fail',
|
|
244
|
+
errors: [
|
|
245
|
+
'brief-validation autonomy clause: the standing autonomy clause ("Do not stop to ask clarifying questions...") was not found in the PR body.'
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Project field — required in a multi-project repo (`brief-authoring` skill,
|
|
252
|
+
* `planner-brief` contract: the field routes the Developer to the right specs
|
|
253
|
+
* and tells the Reviewer whose behavior to verify). Must appear in the header
|
|
254
|
+
* block, where the Archivist's provenance assembly also reads it — gate and
|
|
255
|
+
* archivist share `headerRegion`, so a body that passes this gate can never
|
|
256
|
+
* produce a DANGLING Project field in provenance. Added after #311 merged
|
|
257
|
+
* without it: the Developer satisfied exactly the sections this gate checked
|
|
258
|
+
* and dropped everything it didn't — whatever the gate doesn't enforce, agents
|
|
259
|
+
* will eventually omit (contract-gate parity is the fix, not discipline).
|
|
260
|
+
*/
|
|
261
|
+
export function checkProjectField(prBody: string): BriefSectionResult {
|
|
262
|
+
if (headerField(prBody, 'Project(?:\\(s\\))?', 'PROJECT') !== null) return { status: 'pass', errors: [] }
|
|
263
|
+
return {
|
|
264
|
+
status: 'fail',
|
|
265
|
+
errors: [
|
|
266
|
+
'brief-validation Project: no `Project:` field found in the PR body header block (before the first `##` heading). Required per the brief-developer contract — expected `Project: <name>[, <name>]` or `**Project:** …`.'
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* For/model field — the brief's mandatory `For:` header (`brief-authoring`
|
|
273
|
+
* skill: "the `For:` + `Reason:` lines are mandatory"). AEG forbids
|
|
274
|
+
* commit-trailer attribution, so this line is the provenance block's only
|
|
275
|
+
* source for Model/agent — same headerRegion parity rationale as
|
|
276
|
+
* `checkProjectField`.
|
|
277
|
+
*/
|
|
278
|
+
export function checkForField(prBody: string): BriefSectionResult {
|
|
279
|
+
if (headerField(prBody, 'For') !== null) return { status: 'pass', errors: [] }
|
|
280
|
+
return {
|
|
281
|
+
status: 'fail',
|
|
282
|
+
errors: [
|
|
283
|
+
'brief-validation For: no `For:` field found in the PR body header block (before the first `##` heading). Required per the brief-authoring skill — expected `For: <model + environment>` or `**For:** …`.'
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* `Closes #N` gate — the match runs on **code-stripped** body text (fenced
|
|
290
|
+
* blocks + inline spans removed via `stripCode`), so it agrees byte-for-byte
|
|
291
|
+
* with GitHub's own auto-close parser, which also ignores `Closes #N` inside
|
|
292
|
+
* code. Without the strip, a body whose only closing reference is backticked
|
|
293
|
+
* (`` `Closes #600` ``) passed this gate green yet merged **without** closing
|
|
294
|
+
* its Issue — stranding #600 (PR #608) and #601 (PR #611) and reddening every
|
|
295
|
+
* open PR via the A3 `auto-close-misfire` oracle. "verify-docs green" must
|
|
296
|
+
* imply "GitHub will auto-close"; stripping code here is what makes it so.
|
|
297
|
+
*
|
|
298
|
+
* The separator groups are **bounded** (`\s{0,8}`, not `\s*`). Two adjacent
|
|
299
|
+
* unbounded `\s*` around an optional `:` backtrack quadratically on a body of
|
|
300
|
+
* the shape `closes` + long whitespace + no `#` — ~2.65 s at GitHub's
|
|
301
|
+
* 65,536-char body cap, and this function runs the pattern twice on the fail
|
|
302
|
+
* path (PR #617 security pass). A constant bound makes the work per start
|
|
303
|
+
* position constant, so the scan is linear in body length. Eight is far past
|
|
304
|
+
* any real separator; a body needing more is malformed by the brief's own
|
|
305
|
+
* convention (a bare ref inside the `AEG:CLOSES` anchor) and fails with an
|
|
306
|
+
* actionable message rather than silently stranding its Issue.
|
|
307
|
+
*/
|
|
308
|
+
export function checkClosesN(prBody: string): BriefSectionResult {
|
|
309
|
+
const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i
|
|
310
|
+
if (closesPattern.test(stripCode(prBody))) {
|
|
311
|
+
return { status: 'pass', errors: [] }
|
|
312
|
+
}
|
|
313
|
+
// Distinguish "only inside code" (actionable — move it out) from "absent entirely".
|
|
314
|
+
if (closesPattern.test(prBody)) {
|
|
315
|
+
return {
|
|
316
|
+
status: 'fail',
|
|
317
|
+
errors: [
|
|
318
|
+
"brief-validation Closes #N: `Closes #N` found only inside a code span — GitHub won't auto-close it. Put a bare `Closes #N` on its own line inside the `AEG:CLOSES` anchor."
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
status: 'fail',
|
|
324
|
+
errors: ['brief-validation Closes #N: no `Closes #<N>` (or Fixes/Resolves) reference found in the PR body.']
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The commit-type vocabulary this repo enforces — the commitlint type set
|
|
330
|
+
* plus `Plan` (plan PRs; deliberately excluded from the separate
|
|
331
|
+
* `vinaya/type:*` label axis, since a commit type and a task type are
|
|
332
|
+
* different things — see `packages/aeg-forge-state/src/labels.ts`). The one
|
|
333
|
+
* list: `COMMIT_TYPE_STYLE` below is derived from it, and the `commit-msg`
|
|
334
|
+
* hook (`apps/cli/src/commands/commit-msg.ts`) reads `COMMIT_TYPES` for its
|
|
335
|
+
* own error text — neither carries a second, independently-typed copy.
|
|
336
|
+
*/
|
|
337
|
+
export const COMMIT_TYPES = [
|
|
338
|
+
'Build',
|
|
339
|
+
'Chore',
|
|
340
|
+
'Docs',
|
|
341
|
+
'Feat',
|
|
342
|
+
'Fix',
|
|
343
|
+
'Perf',
|
|
344
|
+
'Plan',
|
|
345
|
+
'Refactor',
|
|
346
|
+
'Revert',
|
|
347
|
+
'Style',
|
|
348
|
+
'Test'
|
|
349
|
+
] as const
|
|
350
|
+
|
|
351
|
+
export const COMMIT_TYPE_STYLE = new RegExp(`^(${COMMIT_TYPES.join('|')})(\\([a-z0-9-]+\\))?: \\S`)
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Forge-title grammar — the two title forms this repo actually uses:
|
|
355
|
+
* 1. Commit-style: `Type: description` or `Type(scope): description`, with
|
|
356
|
+
* the commitlint type set plus `Plan` (plan PRs).
|
|
357
|
+
* 2. Task-style: `[tranche] id — description` (task Issues and task PRs).
|
|
358
|
+
* Anything else is refused: titles ride into merge commits and the forge's
|
|
359
|
+
* derived views, so they carry the same grammar obligation as commit messages
|
|
360
|
+
* (husky/commitlint parity, applied at the wrapper).
|
|
361
|
+
*/
|
|
362
|
+
export function checkForgeTitle(title: string): BriefSectionResult {
|
|
363
|
+
const taskStyle = /^\[[a-z0-9._-]+\] \S+ — \S/
|
|
364
|
+
if (COMMIT_TYPE_STYLE.test(title) || taskStyle.test(title)) return { status: 'pass', errors: [] }
|
|
365
|
+
return {
|
|
366
|
+
status: 'fail',
|
|
367
|
+
errors: [
|
|
368
|
+
`brief-validation title: "${title}" matches neither title grammar — expected \`Type: description\` / \`Type(scope): description\` (commitlint types + Plan) or \`[tranche] id — description\` (task form).`
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Plan-PR Closes guard — a `plan/*` PR body must never carry a
|
|
375
|
+
* `Closes #N` reference. Three confirmed live incidents (#294→#293,
|
|
376
|
+
* #298→#297, #288→#287) show a plan PR's `Closes #N` prematurely closing
|
|
377
|
+
* the task Issue when the *plan* merged — before the task itself ever ran.
|
|
378
|
+
* `roles/planner.md`'s Plan-PR close-out section already forbids this in
|
|
379
|
+
* prose ("a plan PR creates Issues; it does not resolve one"); this is the
|
|
380
|
+
* mechanical gate. Separate from `checkBriefSections` (it needs the branch,
|
|
381
|
+
* not just the body, and it's a guard against a forbidden shape, not a
|
|
382
|
+
* presence check) — the shim runs it before the non-task-branch bypass, since
|
|
383
|
+
* a `plan/*` branch would otherwise never reach a brief-shape check at all.
|
|
384
|
+
* Plan branches without `Closes` continue to bypass, as today.
|
|
385
|
+
*/
|
|
386
|
+
export function checkPlanPrNoCloses(branch: string, prBody: string): BriefSectionResult {
|
|
387
|
+
if (!branch.startsWith('plan/')) return { status: 'pass', errors: [] }
|
|
388
|
+
if (!/\bcloses\s+#\d+/i.test(prBody)) return { status: 'pass', errors: [] }
|
|
389
|
+
return {
|
|
390
|
+
status: 'fail',
|
|
391
|
+
errors: [
|
|
392
|
+
'brief-validation plan-PR guard: a `plan/*` PR must not carry `Closes #N` — a plan PR creates Issues, it does not resolve one (roles/planner.md). Remove the `Closes #N` reference; the task Issue closes from the task PR that does the work.'
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The four markers that make a body *a brief* rather than an ordinary PR
|
|
399
|
+
* description. Each is an existing section detector, reused unchanged — so a
|
|
400
|
+
* body that trips this detector is, by construction, a body the section checks
|
|
401
|
+
* were written to grade.
|
|
402
|
+
*
|
|
403
|
+
* Test Plan is deliberately **not** a marker even though it is a required brief
|
|
404
|
+
* section: the canonical PR-body template gives every AEG PR a Test plan with
|
|
405
|
+
* `**[agent]**` tags, so it is the one section an ordinary non-brief PR
|
|
406
|
+
* plausibly carries. The four kept here are brief grammar and nothing else — a
|
|
407
|
+
* dependency bump has none of them, and needing *two* means one stray phrase
|
|
408
|
+
* can never force a brief onto a PR that was never meant to carry one.
|
|
409
|
+
*/
|
|
410
|
+
const BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause] as const
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* A task branch, per the topology naming convention (`task/<tranche>/<n>`).
|
|
414
|
+
* The one shared copy — `bin/verify-brief.ts`, `test-plan-gate.ts`, and
|
|
415
|
+
* `archive-task.ts` each still define this pattern locally (out of this
|
|
416
|
+
* export's blast radius; not deduped onto it here), but a new consumer
|
|
417
|
+
* (`check-brief-shape.ts`, #870) reuses this one rather than adding a
|
|
418
|
+
* fourth copy.
|
|
419
|
+
*/
|
|
420
|
+
const TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/
|
|
421
|
+
|
|
422
|
+
export function isTaskBranch(branch: string): boolean {
|
|
423
|
+
return TASK_BRANCH_PATTERN.test(branch)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Is this PR body a brief? — the predicate that replaces the branch name as
|
|
428
|
+
* `verify-brief`'s trigger for running `checkBriefSections`.
|
|
429
|
+
*
|
|
430
|
+
* The old rule was "validate iff the branch is `task/<tranche>/<n>`", which meant a
|
|
431
|
+
* standalone `fix/*` brief bypassed **every** section check. Confirmed live: a
|
|
432
|
+
* fix brief on `fix/studio-tranche-href` shipped with no §7
|
|
433
|
+
* documentation-update list, and `checkDocUpdateList` — the checker that exists
|
|
434
|
+
* for exactly that — never ran, because the branch wasn't a task branch. The
|
|
435
|
+
* exemption still has to exist (an ordinary one-line dependency-bump PR has no
|
|
436
|
+
* brief and must not be forced to grow one), so the rule becomes: *if a body is
|
|
437
|
+
* a brief, it must be a complete brief, whatever the branch is called.*
|
|
438
|
+
*
|
|
439
|
+
* Detection runs on `stripCode(prBody)` (the single shared stripper from
|
|
440
|
+
* `anchored-region`, per #617's "one stripper, never a duplicated regex"). A PR
|
|
441
|
+
* that *quotes* a brief inside a fence — "here's a sample brief: ``` …Technical
|
|
442
|
+
* surface map… ```" — is discussing a brief, not carrying one, and must stay
|
|
443
|
+
* exempt; matching on raw text would force-validate it. Note this also rules out
|
|
444
|
+
* the worktree `git worktree add … -b` line as a marker: Step 0 lives inside a
|
|
445
|
+
* fence in every real brief, so it never survives the strip.
|
|
446
|
+
*
|
|
447
|
+
* Threshold is ≥2 of four rather than any-one so that a brief missing one
|
|
448
|
+
* section is still detected as a brief — the failure mode this gate exists to
|
|
449
|
+
* catch. The `fix/studio-tranche-href` body trips three with §7 absent.
|
|
450
|
+
*/
|
|
451
|
+
export function isBriefShaped(prBody: string): boolean {
|
|
452
|
+
const stripped = stripCode(prBody)
|
|
453
|
+
return BRIEF_SHAPE_MARKERS.filter((check) => check(stripped).status === 'pass').length >= 2
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The branch a brief declares for itself, read from its Step 0 command. At
|
|
458
|
+
* authoring time there is no `BRANCH` env var and no branch yet — but every
|
|
459
|
+
* brief carries `git worktree add <path> -b <branch> origin/main`, so the brief
|
|
460
|
+
* states what it is going to be, and `verify-brief --body-file` can grade it as
|
|
461
|
+
* that. Matched on the RAW body, not `stripCode`'s output: Step 0 lives inside a
|
|
462
|
+
* fence in every real brief, so the stripped text never contains it.
|
|
463
|
+
*/
|
|
464
|
+
export function inferBranchFromBody(prBody: string): string {
|
|
465
|
+
const m = prBody.match(/git worktree add\s+\S+\s+-b\s+(\S+)/)
|
|
466
|
+
return m?.[1] ?? ''
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Composition knobs for `checkBriefSections` — see each field. */
|
|
470
|
+
export type BriefSectionsOptions = {
|
|
471
|
+
/**
|
|
472
|
+
* Whether a `Closes #N` reference is required. Defaults to `true` (every
|
|
473
|
+
* existing caller keeps today's behavior).
|
|
474
|
+
*
|
|
475
|
+
* A task PR must close its Issue, so `verify-brief` leaves this on for
|
|
476
|
+
* `task/<tranche>/<n>`. A brief-shaped body on a non-task branch must not: a
|
|
477
|
+
* standalone fix brief has no task Issue to close, and a `plan/*` PR is
|
|
478
|
+
* *forbidden* to carry `Closes #N` by `checkPlanPrNoCloses` — so
|
|
479
|
+
* requiring it there would make the two gates jointly unsatisfiable. Issue
|
|
480
|
+
* linkage is a task-branch obligation; brief completeness is not.
|
|
481
|
+
*/
|
|
482
|
+
requireClosesN?: boolean
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Aggregates every section detector into one combined result — one error
|
|
487
|
+
* line per failing section, mirroring `doc-owners.ts`'s `parseDocOwners`
|
|
488
|
+
* error-message style.
|
|
489
|
+
*/
|
|
490
|
+
export function checkBriefSections(
|
|
491
|
+
prBody: string,
|
|
492
|
+
readTier: (body: string) => 0 | 1 | 3 | null,
|
|
493
|
+
options: BriefSectionsOptions = {}
|
|
494
|
+
): { errors: string[] } {
|
|
495
|
+
const { requireClosesN = true } = options
|
|
496
|
+
const results = [
|
|
497
|
+
checkTierField(prBody, readTier),
|
|
498
|
+
checkTestPlan(prBody),
|
|
499
|
+
checkTestPlanExclusivity(prBody),
|
|
500
|
+
checkPrincipalPlaceholder(prBody),
|
|
501
|
+
checkSurfaceMap(prBody),
|
|
502
|
+
checkDocUpdateList(prBody),
|
|
503
|
+
checkWorktreeStep0(prBody),
|
|
504
|
+
checkStopConditions(prBody),
|
|
505
|
+
checkAutonomyClause(prBody),
|
|
506
|
+
checkProjectField(prBody),
|
|
507
|
+
checkForField(prBody),
|
|
508
|
+
...(requireClosesN ? [checkClosesN(prBody)] : [])
|
|
509
|
+
]
|
|
510
|
+
return { errors: results.flatMap((r) => r.errors) }
|
|
511
|
+
}
|