@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,787 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-coherence pure check evaluators — deterministic plan↔forge coherence
|
|
3
|
+
* oracle logic (A1/A2/A3, T1/T2/T3, D1, L1/L2/L3/L4, closes-N)..
|
|
4
|
+
*
|
|
5
|
+
* Pure — no `fs`, no `fetch`, no `process.env`. All forge facts and tranche
|
|
6
|
+
* topology are injected by the caller (`bin/verify-coherence.ts`, the I/O shim).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { trancheLabel, label } from '@attalabs/aeg-forge-state'
|
|
10
|
+
import { anchoredRegion, stripCode } from './anchored-region'
|
|
11
|
+
import { checkIssueRationale, checkProjectsRegistered, isTaskIssueLabelSet } from './issue-validation'
|
|
12
|
+
import { isPrincipal, PRINCIPAL_ALLOWLIST } from './waiver-label'
|
|
13
|
+
import type { ForgeIssue, TaskIssueRef } from '@attalabs/aeg-types'
|
|
14
|
+
import type { ForgeFacts, Tranche, Task } from './types'
|
|
15
|
+
|
|
16
|
+
// ---------- grandfather cutoff -----------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Incoherences whose terminal forge event predates this date are grandfathered:
|
|
20
|
+
* emitted as `status: "info"` (visible in the report) rather than `"fail"`
|
|
21
|
+
* (which blocks CI). Applies to A1/A2/A3/T3.
|
|
22
|
+
*
|
|
23
|
+
* Rationale: pre-existing repo-wide debt from tranches that predate the
|
|
24
|
+
* cutoff can't be retro-fixed, so a hard gate on those findings would make
|
|
25
|
+
* every new PR un-mergeable.
|
|
26
|
+
*/
|
|
27
|
+
export const COHERENCE_ENFORCED_FROM = '2026-07-01'
|
|
28
|
+
|
|
29
|
+
/** True when `isoDate` is an ISO string whose date portion is strictly before `COHERENCE_ENFORCED_FROM`. */
|
|
30
|
+
export function isGrandfathered(isoDate: string | null | undefined): boolean {
|
|
31
|
+
if (!isoDate) return false
|
|
32
|
+
return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* R1 grandfather — explicit, data-declared Issue numbers whose body predates
|
|
37
|
+
* the rationale grammar (or predates R1 enforcement) and is therefore
|
|
38
|
+
* exempted from blocking. Unlike A1/A2/A3/T3's date-based cutoff, an Issue
|
|
39
|
+
* body carries no reliable "authored under which grammar" timestamp, so this
|
|
40
|
+
* is an explicit number set rather than a date proxy — populated once, at
|
|
41
|
+
* `aeg-governance-hardening` task 1 implementation time, with exactly the
|
|
42
|
+
* active-tranche task Issues that failed `checkIssueRationale` against the
|
|
43
|
+
* live forge (see the task's PR body for the list + counts).
|
|
44
|
+
*
|
|
45
|
+
* New/edited task Issues are already gated at ring 0 (`bin/open-issue.ts`,
|
|
46
|
+
*) — this list is visible debt for the pre-gate stock, not a standing
|
|
47
|
+
* exemption mechanism. Do not add to it going forward; fix the Issue body
|
|
48
|
+
* instead (the planner-brief rationale contract).
|
|
49
|
+
*/
|
|
50
|
+
export const R1_GRANDFATHERED_ISSUES: ReadonlySet<number> = new Set([279, 280, 281, 282])
|
|
51
|
+
|
|
52
|
+
// ---------- types -------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export type CheckFailure = {
|
|
55
|
+
issue?: number | null
|
|
56
|
+
tranche: string
|
|
57
|
+
task?: string
|
|
58
|
+
reason: string
|
|
59
|
+
/** True when the terminal event predates `COHERENCE_ENFORCED_FROM` — finding is info, not fail. */
|
|
60
|
+
grandfathered?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type CheckResult = {
|
|
64
|
+
check: string
|
|
65
|
+
status: 'pass' | 'fail' | 'info'
|
|
66
|
+
failures: CheckFailure[]
|
|
67
|
+
note?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type TaskEntry = {
|
|
71
|
+
trancheSlug: string
|
|
72
|
+
archived: boolean
|
|
73
|
+
task: Task
|
|
74
|
+
/** `undefined` when the forge was unavailable or the issue didn't exist. */
|
|
75
|
+
facts: ForgeFacts | undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type TrancheFile = {
|
|
79
|
+
slug: string
|
|
80
|
+
archived: boolean
|
|
81
|
+
tranche: Tranche
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------- pure check evaluators --------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A1: Every closed task-Issue has a merged closing PR — OR was hand-closed
|
|
88
|
+
* directly by a recognized Principal (task `vinaya-engine-v1` 21, #99): a
|
|
89
|
+
* second, narrower "done" path for a dependency Issue whose technical
|
|
90
|
+
* premise dissolved, closed with a stated `COMPLETED` reason rather than via
|
|
91
|
+
* a merge. Every condition is a real forge fact (who performed the
|
|
92
|
+
* CLOSED_EVENT, GitHub's own close reason), never a prose claim in a
|
|
93
|
+
* comment — the trap this task exists to avoid is "any closed Issue with a
|
|
94
|
+
* comment counts."
|
|
95
|
+
* Fail class: `closed-without-merge`
|
|
96
|
+
* Terminal event date: `issueClosedAt` — grandfathered when before `COHERENCE_ENFORCED_FROM`.
|
|
97
|
+
*
|
|
98
|
+
* Excludes `stateReason: 'not_planned'`: a task closed that way with no merged
|
|
99
|
+
* PR is `dropped` — legitimately abandoned, never done, never `todo` — a valid
|
|
100
|
+
* terminal state, not a coherence failure. `stateReason: 'completed'` (or
|
|
101
|
+
* `null`) with no merged PR and no recognized hand-close stays flagged: that
|
|
102
|
+
* is done-but-unprovable, or a broken close, exactly the class this check
|
|
103
|
+
* exists to catch.
|
|
104
|
+
*/
|
|
105
|
+
export function checkA1(entries: TaskEntry[], principalAllowlist: string[] = PRINCIPAL_ALLOWLIST): CheckResult {
|
|
106
|
+
const failures: CheckFailure[] = []
|
|
107
|
+
for (const e of entries) {
|
|
108
|
+
if (!e.facts) continue
|
|
109
|
+
if (e.facts.stateReason === 'not_planned') continue
|
|
110
|
+
if (e.facts.issueState === 'closed' && e.facts.prState !== 'merged') {
|
|
111
|
+
const handClosed = e.facts.stateReason === 'completed' && isPrincipal(e.facts.closedByActor, principalAllowlist)
|
|
112
|
+
if (handClosed) continue
|
|
113
|
+
failures.push({
|
|
114
|
+
issue: e.task.issue,
|
|
115
|
+
tranche: e.trancheSlug,
|
|
116
|
+
task: e.task.id,
|
|
117
|
+
reason: `Issue closed but closing PR is not merged (prState: ${e.facts.prState})`,
|
|
118
|
+
grandfathered: isGrandfathered(e.facts.closedAt)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const activeFails = failures.filter((f) => !f.grandfathered)
|
|
123
|
+
const status = activeFails.length > 0 ? 'fail' : failures.length > 0 ? 'info' : 'pass'
|
|
124
|
+
return {
|
|
125
|
+
check: 'A1',
|
|
126
|
+
status,
|
|
127
|
+
failures,
|
|
128
|
+
note: status === 'info' ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A2: The closing PR of each closed task-Issue carries an Archivist provenance block.
|
|
134
|
+
* Fail class: `archived-without-provenance`
|
|
135
|
+
* Terminal event date: `prMergedAt` — grandfathered when before `COHERENCE_ENFORCED_FROM`.
|
|
136
|
+
*
|
|
137
|
+
* `hasProvenanceByKey`: Map keyed by `${trancheSlug}/${taskId}` → true when the
|
|
138
|
+
* closing PR has a comment containing `### AEG provenance`.
|
|
139
|
+
*/
|
|
140
|
+
export function checkA2(entries: TaskEntry[], hasProvenanceByKey: Map<string, boolean>): CheckResult {
|
|
141
|
+
const failures: CheckFailure[] = []
|
|
142
|
+
for (const e of entries) {
|
|
143
|
+
if (!e.facts) continue
|
|
144
|
+
// Only check tasks whose issue is closed AND whose closing PR merged.
|
|
145
|
+
if (e.facts.issueState !== 'closed' || e.facts.prState !== 'merged') continue
|
|
146
|
+
const key = `${e.trancheSlug}/${e.task.id}`
|
|
147
|
+
const hasProvenance = hasProvenanceByKey.get(key)
|
|
148
|
+
// If key is absent from the map we were unable to fetch (forge error); skip.
|
|
149
|
+
if (hasProvenance === undefined) continue
|
|
150
|
+
if (!hasProvenance) {
|
|
151
|
+
failures.push({
|
|
152
|
+
issue: e.task.issue,
|
|
153
|
+
tranche: e.trancheSlug,
|
|
154
|
+
task: e.task.id,
|
|
155
|
+
reason: 'Closing PR has no `### AEG provenance` comment (Archivist close-out missing)',
|
|
156
|
+
grandfathered: isGrandfathered(e.facts.mergedAt)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const activeFails = failures.filter((f) => !f.grandfathered)
|
|
161
|
+
const status = activeFails.length > 0 ? 'fail' : failures.length > 0 ? 'info' : 'pass'
|
|
162
|
+
return {
|
|
163
|
+
check: 'A2',
|
|
164
|
+
status,
|
|
165
|
+
failures,
|
|
166
|
+
note: status === 'info' ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A3: Every Issue whose closing PR merged is itself closed.
|
|
172
|
+
* Fail class: `auto-close-misfire` — the headline check (#174 class).
|
|
173
|
+
* Terminal event date: `prMergedAt` — grandfathered when before `COHERENCE_ENFORCED_FROM`.
|
|
174
|
+
*/
|
|
175
|
+
export function checkA3(entries: TaskEntry[]): CheckResult {
|
|
176
|
+
const failures: CheckFailure[] = []
|
|
177
|
+
for (const e of entries) {
|
|
178
|
+
if (!e.facts) continue
|
|
179
|
+
if (e.facts.prState === 'merged' && e.facts.issueState !== 'closed') {
|
|
180
|
+
failures.push({
|
|
181
|
+
issue: e.task.issue,
|
|
182
|
+
tranche: e.trancheSlug,
|
|
183
|
+
task: e.task.id,
|
|
184
|
+
reason: 'Closing PR is merged but Issue is still open (GitHub auto-close misfire)',
|
|
185
|
+
grandfathered: isGrandfathered(e.facts.mergedAt)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const activeFails = failures.filter((f) => !f.grandfathered)
|
|
190
|
+
const status = activeFails.length > 0 ? 'fail' : failures.length > 0 ? 'info' : 'pass'
|
|
191
|
+
return {
|
|
192
|
+
check: 'A3',
|
|
193
|
+
status,
|
|
194
|
+
failures,
|
|
195
|
+
note: status === 'info' ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* T1: Every topology row's Issue ref resolves to a real Issue.
|
|
201
|
+
* Fail class: `phantom-issue-ref`
|
|
202
|
+
*
|
|
203
|
+
* A task has a non-null issue number in the topology but is absent from the
|
|
204
|
+
* forge facts map → the issue doesn't exist on GitHub.
|
|
205
|
+
*/
|
|
206
|
+
export function checkT1(entries: TaskEntry[]): CheckResult {
|
|
207
|
+
const failures: CheckFailure[] = []
|
|
208
|
+
for (const e of entries) {
|
|
209
|
+
// Skip: issue was null in topology (TBD or empty); T3 handles that.
|
|
210
|
+
if (e.task.issue === null) continue
|
|
211
|
+
// facts === undefined and issue !== null → forge query returned nothing for this issue
|
|
212
|
+
if (e.facts === undefined) {
|
|
213
|
+
failures.push({
|
|
214
|
+
issue: e.task.issue,
|
|
215
|
+
tranche: e.trancheSlug,
|
|
216
|
+
task: e.task.id,
|
|
217
|
+
reason: `Issue #${e.task.issue} in topology does not resolve to a real GitHub Issue`
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { check: 'T1', status: failures.length > 0 ? 'fail' : 'pass', failures }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* T2: Every open Issue labeled `vinaya/tranche:X` appears in X's topology file.
|
|
226
|
+
* Fail class: `orphan-task`
|
|
227
|
+
*
|
|
228
|
+
* `openIssuesBySlug`: Map from active tranche slug → list of open issue
|
|
229
|
+
* numbers fetched from the forge with that `vinaya/tranche:` label.
|
|
230
|
+
* `topologyIssuesBySlug`: Map from slug → Set of issue numbers in the topology.
|
|
231
|
+
*
|
|
232
|
+
* `ciTrancheSlug`: when set (parsed from `BRANCH`/`GITHUB_HEAD_REF` env),
|
|
233
|
+
* only the tranche matching that slug is checked — prevents a coherence
|
|
234
|
+
* gap in one tranche's topology (e.g. a Planner plan-PR mid-flight) from
|
|
235
|
+
* blocking CI on an unrelated PR against a different tranche. Mirrors
|
|
236
|
+
* `checkT3`'s `ciTrancheSlug` parameter exactly.
|
|
237
|
+
*/
|
|
238
|
+
export function checkT2(
|
|
239
|
+
openIssuesBySlug: Map<string, number[]>,
|
|
240
|
+
topologyIssuesBySlug: Map<string, Set<number>>,
|
|
241
|
+
ciTrancheSlug?: string | null
|
|
242
|
+
): CheckResult {
|
|
243
|
+
const failures: CheckFailure[] = []
|
|
244
|
+
for (const [slug, openNums] of openIssuesBySlug) {
|
|
245
|
+
if (ciTrancheSlug && slug !== ciTrancheSlug) continue
|
|
246
|
+
|
|
247
|
+
const topologySet = topologyIssuesBySlug.get(slug) ?? new Set<number>()
|
|
248
|
+
for (const num of openNums) {
|
|
249
|
+
if (!topologySet.has(num)) {
|
|
250
|
+
failures.push({
|
|
251
|
+
issue: num,
|
|
252
|
+
tranche: slug,
|
|
253
|
+
reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { check: 'T2', status: failures.length > 0 ? 'fail' : 'pass', failures }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* T2 point-of-power relocation (aeg-governance-hardening task 24, #364,
|
|
263
|
+
* Part 2; supersedes half of task 19's T2-in-task-PR-CI placement).
|
|
264
|
+
* A gate may only red a PR that could cause or cure the violation it
|
|
265
|
+
* reports — live incident #363 (2026-07-04): registering Issues #364/#365
|
|
266
|
+
* correctly reddened same-tranche task PR #363's CI, which could neither
|
|
267
|
+
* have caused nor fixed the topology gap. `checkT2`'s own assertion logic
|
|
268
|
+
* (above) is untouched; this only demotes its CI-blocking status when the
|
|
269
|
+
* current PR is NOT a plan PR (i.e. its diff doesn't touch a tranche
|
|
270
|
+
* topology file) — the only PR kind that can actually close a T2 gap. The
|
|
271
|
+
* underlying findings stay visible (`status: 'info'`, never omitted) for
|
|
272
|
+
* every other context: task-PR CI, `--json`/audit mode, and daily-drift.
|
|
273
|
+
*/
|
|
274
|
+
export function scopeT2ToPlanPr(result: CheckResult, isPlanPr: boolean): CheckResult {
|
|
275
|
+
if (isPlanPr || result.status !== 'fail') return result
|
|
276
|
+
return {
|
|
277
|
+
...result,
|
|
278
|
+
status: 'info',
|
|
279
|
+
note:
|
|
280
|
+
result.note ??
|
|
281
|
+
'T2 findings are non-blocking outside plan PRs (point-of-power principle) — see aeg-root/enforcement.md.'
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* T3: No `#TBD` rows in an active tranche.
|
|
287
|
+
* Fail class: `tbd-in-active-tranche`
|
|
288
|
+
*
|
|
289
|
+
* A task in an active tranche has a null issue ref (empty / `—` / `#TBD`).
|
|
290
|
+
*
|
|
291
|
+
* `ciTrancheSlug`: when set (parsed from `BRANCH`/`GITHUB_HEAD_REF` env),
|
|
292
|
+
* only tasks in THAT tranche are checked — prevents legacy `#TBD` rows in
|
|
293
|
+
* an unrelated tranche from blocking this PR.
|
|
294
|
+
*
|
|
295
|
+
* `enrichedEntries`: when provided (post-forge-fetch), used to determine if a
|
|
296
|
+
* tranche predates `COHERENCE_ENFORCED_FROM` by proxy: if the tranche has
|
|
297
|
+
* any task whose `closedAt` or `mergedAt` is pre-cutoff, its #TBD rows are
|
|
298
|
+
* grandfathered as `info`.
|
|
299
|
+
*
|
|
300
|
+
* `forgeUnavailableSlugs`: tranche slugs whose forge snapshot fetch failed
|
|
301
|
+
* entirely (the caller couldn't fetch `closedAt`/`mergedAt` for ANY task in
|
|
302
|
+
* that tranche). A `#TBD` row in one of these tranches cannot be evaluated
|
|
303
|
+
* against the grandfather proxy at all — treating it as `grandfathered: false`
|
|
304
|
+
* would silently fail it purely because of a forge outage, not because it's
|
|
305
|
+
* genuinely un-grandfathered. Such rows are reported `grandfathered: true`
|
|
306
|
+
* with a distinct reason so they never produce a `fail`, but remain visible.
|
|
307
|
+
*/
|
|
308
|
+
export function checkT3(
|
|
309
|
+
entries: TaskEntry[],
|
|
310
|
+
ciTrancheSlug?: string | null,
|
|
311
|
+
enrichedEntries?: TaskEntry[],
|
|
312
|
+
forgeUnavailableSlugs?: Set<string>
|
|
313
|
+
): CheckResult {
|
|
314
|
+
// Build set of tranches that are pre-enforcement (by proxy: any task with a pre-cutoff date).
|
|
315
|
+
const preEnforcement = new Set<string>()
|
|
316
|
+
if (enrichedEntries) {
|
|
317
|
+
for (const e of enrichedEntries) {
|
|
318
|
+
if (isGrandfathered(e.facts?.closedAt) || isGrandfathered(e.facts?.mergedAt)) {
|
|
319
|
+
preEnforcement.add(e.trancheSlug)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const failures: CheckFailure[] = []
|
|
325
|
+
for (const e of entries) {
|
|
326
|
+
if (!e.archived && e.task.issue === null) {
|
|
327
|
+
// Branch-scope: in CI for a specific tranche, only check that tranche.
|
|
328
|
+
if (ciTrancheSlug && e.trancheSlug !== ciTrancheSlug) continue
|
|
329
|
+
|
|
330
|
+
if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
|
|
331
|
+
failures.push({
|
|
332
|
+
issue: null,
|
|
333
|
+
tranche: e.trancheSlug,
|
|
334
|
+
task: e.task.id,
|
|
335
|
+
reason: `Task ${e.task.id} in active tranche has no Issue ref (#TBD or empty), but forge data for tranche "${e.trancheSlug}" was unavailable — cannot evaluate grandfather status, not silently failed`,
|
|
336
|
+
grandfathered: true
|
|
337
|
+
})
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
failures.push({
|
|
342
|
+
issue: null,
|
|
343
|
+
tranche: e.trancheSlug,
|
|
344
|
+
task: e.task.id,
|
|
345
|
+
reason: `Task ${e.task.id} in active tranche has no Issue ref (#TBD or empty) — the model requires all active tasks to have Issue numbers`,
|
|
346
|
+
grandfathered: preEnforcement.has(e.trancheSlug)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const activeFails = failures.filter((f) => !f.grandfathered)
|
|
351
|
+
const status = activeFails.length > 0 ? 'fail' : failures.length > 0 ? 'info' : 'pass'
|
|
352
|
+
return {
|
|
353
|
+
check: 'T3',
|
|
354
|
+
status,
|
|
355
|
+
failures,
|
|
356
|
+
note: status === 'info' ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* D1: A task with an open PR has all `depends-on` Issues closed.
|
|
362
|
+
* Fail class: `dispatched-on-unmet-deps`
|
|
363
|
+
*
|
|
364
|
+
* `issueToEntry`: Map from issue number → TaskEntry, for resolving `#NNN`
|
|
365
|
+
* style depends-on refs in addition to task-ID style refs.
|
|
366
|
+
* `taskToEntry`: Map from `${slug}/${taskId}` → TaskEntry for same-tranche refs.
|
|
367
|
+
*/
|
|
368
|
+
export function checkD1(
|
|
369
|
+
entries: TaskEntry[],
|
|
370
|
+
issueToEntry: Map<number, TaskEntry>,
|
|
371
|
+
taskToEntry: Map<string, TaskEntry>
|
|
372
|
+
): CheckResult {
|
|
373
|
+
const failures: CheckFailure[] = []
|
|
374
|
+
for (const e of entries) {
|
|
375
|
+
if (!e.facts) continue
|
|
376
|
+
if (e.facts.prState !== 'open') continue
|
|
377
|
+
|
|
378
|
+
for (const dep of e.task.dependsOn) {
|
|
379
|
+
const depEntry = resolveDepEntry(dep, e.trancheSlug, issueToEntry, taskToEntry)
|
|
380
|
+
if (!depEntry) continue // unknown dep — not a D1 concern
|
|
381
|
+
|
|
382
|
+
const depFacts = depEntry.facts
|
|
383
|
+
const depClosed = depFacts?.issueState === 'closed'
|
|
384
|
+
if (!depClosed) {
|
|
385
|
+
failures.push({
|
|
386
|
+
issue: e.task.issue,
|
|
387
|
+
tranche: e.trancheSlug,
|
|
388
|
+
task: e.task.id,
|
|
389
|
+
reason: `Task has open PR but depends-on ${dep} (issue #${depEntry.task.issue ?? '?'}) is not closed`
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { check: 'D1', status: failures.length > 0 ? 'fail' : 'pass', failures }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resolveDepEntry(
|
|
398
|
+
dep: string,
|
|
399
|
+
trancheSlug: string,
|
|
400
|
+
issueToEntry: Map<number, TaskEntry>,
|
|
401
|
+
taskToEntry: Map<string, TaskEntry>
|
|
402
|
+
): TaskEntry | undefined {
|
|
403
|
+
// `#NNN` style — resolve by issue number
|
|
404
|
+
const issueMatch = dep.match(/^#(\d+)$/)
|
|
405
|
+
if (issueMatch?.[1]) return issueToEntry.get(Number(issueMatch[1]))
|
|
406
|
+
// Task-ID style — resolve within same tranche first, then globally
|
|
407
|
+
return taskToEntry.get(`${trancheSlug}/${dep}`) ?? taskToEntry.get(dep)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* `ForgeIssue` lives in `@attalabs/aeg-types` (aeg-core-purity fix, #521) —
|
|
412
|
+
* re-exported here since every existing call site imports it from
|
|
413
|
+
* `@attalabs/aeg-core`.
|
|
414
|
+
*/
|
|
415
|
+
export type { ForgeIssue }
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* R1: Every active-tranche task Issue's body carries the full eight-field
|
|
419
|
+
* Planner's rationale (`aeg-root/contracts/planner-brief.md`), and every
|
|
420
|
+
* project its `Project:` field names has a `.vinaya/projects.md` row.
|
|
421
|
+
* Fail class: `missing-rationale-field`
|
|
422
|
+
*
|
|
423
|
+
* Presence-only — delegates entirely to `checkIssueRationale` and
|
|
424
|
+
* `checkProjectsRegistered` (the same evaluators `bin/open-issue.ts` enforces
|
|
425
|
+
* at ring 0 on new/edited Issues). This is the ring-1/2 half: continuous
|
|
426
|
+
* re-checking of the stock, which is what catches an Issue edited by an
|
|
427
|
+
* ungated writer (the GitHub web UI, a raw API call) or one that predates the
|
|
428
|
+
* gate. One grammar, one parser — this function re-implements neither.
|
|
429
|
+
*
|
|
430
|
+
* `issuesBySlug`: open Issues per active tranche slug, from the same
|
|
431
|
+
* batched label-scoped query T2 uses (`fetchOpenIssuesByLabel`), extended to
|
|
432
|
+
* carry `body` + `labels`.
|
|
433
|
+
* `grandfatheredIssues`: `R1_GRANDFATHERED_ISSUES` — pre- stock,
|
|
434
|
+
* reported as `info`, never `fail`.
|
|
435
|
+
* `registeredNames`: the registry's project names, read by the caller
|
|
436
|
+
* (aeg-core is pure). Defaults to `[]`, which leaves the registry half
|
|
437
|
+
* dormant — so a caller that has no registry to hand keeps R1's prior
|
|
438
|
+
* behaviour exactly.
|
|
439
|
+
*/
|
|
440
|
+
export function checkR1(
|
|
441
|
+
issuesBySlug: Map<string, ForgeIssue[]>,
|
|
442
|
+
grandfatheredIssues: ReadonlySet<number>,
|
|
443
|
+
registeredNames: string[] = []
|
|
444
|
+
): CheckResult {
|
|
445
|
+
const failures: CheckFailure[] = []
|
|
446
|
+
for (const [slug, issues] of issuesBySlug) {
|
|
447
|
+
for (const issue of issues) {
|
|
448
|
+
if (!isTaskIssueLabelSet(issue.labels)) continue
|
|
449
|
+
const errors = [
|
|
450
|
+
...checkIssueRationale(issue.body).errors,
|
|
451
|
+
...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors
|
|
452
|
+
]
|
|
453
|
+
if (errors.length === 0) continue
|
|
454
|
+
failures.push({
|
|
455
|
+
issue: issue.number,
|
|
456
|
+
tranche: slug,
|
|
457
|
+
reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(' | ')}`,
|
|
458
|
+
grandfathered: grandfatheredIssues.has(issue.number)
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const activeFails = failures.filter((f) => !f.grandfathered)
|
|
463
|
+
const status = activeFails.length > 0 ? 'fail' : failures.length > 0 ? 'info' : 'pass'
|
|
464
|
+
return {
|
|
465
|
+
check: 'R1',
|
|
466
|
+
status,
|
|
467
|
+
failures,
|
|
468
|
+
note:
|
|
469
|
+
status === 'info'
|
|
470
|
+
? `${failures.length} grandfathered task Issue(s) predate this check's grammar (rationale fields and/or the project registry)`
|
|
471
|
+
: undefined
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* L1: Active tranche with zero open task-Issues → should be archived.
|
|
477
|
+
* **Advisory (info-only)** per `state-machine.md` §12 (L1/L2 are lifecycle-hygiene
|
|
478
|
+
* signals, not the done-lifecycle gate). Findings are surfaced for a human to
|
|
479
|
+
* investigate; they never fail CI. Only A1/A2/A3/M1/M3/L5 block (L5 promoted, vinaya-milestone-model-v1 task 1).
|
|
480
|
+
*
|
|
481
|
+
* An active tranche (file not in completed/) where every task with a
|
|
482
|
+
* known issue has `issueState === 'closed'`.
|
|
483
|
+
*/
|
|
484
|
+
export function checkL1(files: TrancheFile[], entriesBySlug: Map<string, TaskEntry[]>): CheckResult {
|
|
485
|
+
const failures: CheckFailure[] = []
|
|
486
|
+
for (const f of files) {
|
|
487
|
+
if (f.archived) continue
|
|
488
|
+
const entries = entriesBySlug.get(f.slug) ?? []
|
|
489
|
+
const withFacts = entries.filter((e) => e.facts !== undefined)
|
|
490
|
+
if (withFacts.length === 0) continue // forge unavailable or no tasks with issues
|
|
491
|
+
const allClosed = withFacts.every((e) => e.facts?.issueState === 'closed')
|
|
492
|
+
if (allClosed) {
|
|
493
|
+
failures.push({
|
|
494
|
+
tranche: f.slug,
|
|
495
|
+
reason: 'Active tranche has no open task-Issues — consider archiving to completed/'
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
check: 'L1',
|
|
501
|
+
status: 'info',
|
|
502
|
+
failures,
|
|
503
|
+
note:
|
|
504
|
+
failures.length > 0
|
|
505
|
+
? `${failures.length} active tranche(s) with no open task-Issues — consider archiving (advisory)`
|
|
506
|
+
: undefined
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* L2: Archived tranche with any open task-Issue → premature archive.
|
|
512
|
+
* **Advisory (info-only)** per `state-machine.md` §12 (L1/L2 are lifecycle-hygiene
|
|
513
|
+
* signals, not the done-lifecycle gate). Findings are surfaced for a human to
|
|
514
|
+
* investigate; they never fail CI. Only A1/A2/A3/M1/M3/L5 block (L5 promoted, vinaya-milestone-model-v1 task 1).
|
|
515
|
+
*/
|
|
516
|
+
export function checkL2(files: TrancheFile[], entriesBySlug: Map<string, TaskEntry[]>): CheckResult {
|
|
517
|
+
const failures: CheckFailure[] = []
|
|
518
|
+
for (const f of files) {
|
|
519
|
+
if (!f.archived) continue
|
|
520
|
+
const entries = entriesBySlug.get(f.slug) ?? []
|
|
521
|
+
for (const e of entries) {
|
|
522
|
+
if (!e.facts) continue
|
|
523
|
+
if (e.facts.issueState === 'open') {
|
|
524
|
+
failures.push({
|
|
525
|
+
issue: e.task.issue,
|
|
526
|
+
tranche: f.slug,
|
|
527
|
+
task: e.task.id,
|
|
528
|
+
reason: `Archived tranche has open task-Issue #${e.task.issue ?? '?'} (premature archive)`
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
check: 'L2',
|
|
535
|
+
status: 'info',
|
|
536
|
+
failures,
|
|
537
|
+
note:
|
|
538
|
+
failures.length > 0
|
|
539
|
+
? `${failures.length} archived tranche(s) with an open task-Issue — investigate (advisory)`
|
|
540
|
+
: undefined
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* L3: Count of active tranches — informational only, does not affect exit code.
|
|
546
|
+
*/
|
|
547
|
+
export function checkL3(files: TrancheFile[]): CheckResult {
|
|
548
|
+
const active = files.filter((f) => !f.archived)
|
|
549
|
+
return {
|
|
550
|
+
check: 'L3',
|
|
551
|
+
status: 'info',
|
|
552
|
+
failures: [],
|
|
553
|
+
note: `${active.length} active tranche(s): ${active.map((f) => f.slug).join(', ') || '(none)'}`
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* L4: Issue-level Milestone-attachment drift (aeg-review-gate-v1 task 1
|
|
559
|
+
* follow-up). An open task-Issue carrying `vinaya/tranche:<slug>` for an ACTIVE
|
|
560
|
+
* tranche (open Milestone titled the slug) whose GitHub-native
|
|
561
|
+
* `milestone` field doesn't match that same Milestone.
|
|
562
|
+
*
|
|
563
|
+
* **Scope changed (vinaya-milestone-model-v1 task 1): the exact-title-match
|
|
564
|
+
* invariant this check evaluates is only meaningful for LEGACY tranches** —
|
|
565
|
+
* a Milestone titled exactly the tranche slug, the 1:1 regime that predates
|
|
566
|
+
* the label model. Once one Milestone can legitimately hold several
|
|
567
|
+
* tranches, an Issue attached to a shared Milestone will never have a
|
|
568
|
+
* `milestone.title` equal to any one of the several slugs it might carry,
|
|
569
|
+
* so flagging that as drift would be noise on every label-only tranche. The
|
|
570
|
+
* restriction to legacy slugs is applied by the CALLER
|
|
571
|
+
* (`verify-coherence.ts`, via `TrancheMilestoneIndex.legacySlugs`) — this
|
|
572
|
+
* function's own logic is unchanged and still trusts whatever
|
|
573
|
+
* `activeTrancheSlugs` it is handed.
|
|
574
|
+
*
|
|
575
|
+
* **Advisory (info-only)**, same framing as L1/L2 (`state-machine.md` §12):
|
|
576
|
+
* confirmed NOT functionally load-bearing — `deriveTrancheFromForge`/
|
|
577
|
+
* `listActiveTrancheSlugs` never read an Issue's milestone field, only the
|
|
578
|
+
* `vinaya/tranche:<slug>` label, which remains the sole, sufficient membership
|
|
579
|
+
* signal. This is real drift between GitHub's own Milestone view (e.g.
|
|
580
|
+
* `open_issues`/`closed_issues` counts) and reality — cosmetic, not a gate,
|
|
581
|
+
* surfaced so it doesn't silently accumulate rather than because anything
|
|
582
|
+
* downstream currently breaks. `open-issue.ts` prevents new drift at
|
|
583
|
+
* creation time (ring 0); this is the ring-1/2 detection half for whatever
|
|
584
|
+
* predates that gate or was created outside it.
|
|
585
|
+
*/
|
|
586
|
+
export function checkL4(
|
|
587
|
+
activeTrancheSlugs: string[],
|
|
588
|
+
issueMilestones: Array<{ tranche: string; issue: number; milestoneTitle: string | null }>
|
|
589
|
+
): CheckResult {
|
|
590
|
+
const activeSet = new Set(activeTrancheSlugs)
|
|
591
|
+
const failures: CheckFailure[] = []
|
|
592
|
+
for (const f of issueMilestones) {
|
|
593
|
+
if (!activeSet.has(f.tranche)) continue
|
|
594
|
+
if (f.milestoneTitle === f.tranche) continue
|
|
595
|
+
failures.push({
|
|
596
|
+
issue: f.issue,
|
|
597
|
+
tranche: f.tranche,
|
|
598
|
+
reason:
|
|
599
|
+
f.milestoneTitle === null
|
|
600
|
+
? `Issue #${f.issue} carries ${trancheLabel(f.tranche)} (active) but has no GitHub-native milestone attached`
|
|
601
|
+
: `Issue #${f.issue} carries ${trancheLabel(f.tranche)} (active) but is attached to Milestone "${f.milestoneTitle}" instead`
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
check: 'L4',
|
|
606
|
+
status: 'info',
|
|
607
|
+
failures,
|
|
608
|
+
note:
|
|
609
|
+
failures.length > 0
|
|
610
|
+
? `${failures.length} open task-Issue(s) in an active tranche whose GitHub-native milestone doesn't match their ${label('tranche')}<slug> label (cosmetic drift, advisory)`
|
|
611
|
+
: undefined
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* L5: an active tranche whose every task Issue is closed → the tranche is
|
|
617
|
+
* effectively complete but was never archived (Issue #481, drift class #2;
|
|
618
|
+
* 1 live incident this session — `aeg-forge-state-v1`'s Milestone left open
|
|
619
|
+
* after full archive).
|
|
620
|
+
*
|
|
621
|
+
* This is the FORGE-NATIVE analogue of file-based L1: L1 reads `TrancheFile[]`
|
|
622
|
+
* (a `!f.archived` file location), but post-cutover most tranches have no
|
|
623
|
+
* topology file at all, so their Milestone-object drift is invisible to L1.
|
|
624
|
+
* L5 keys off `activeTrancheSlugs` (the derived-active population — the
|
|
625
|
+
* authority) instead, so it sees exactly the tranches L1 no longer can.
|
|
626
|
+
*
|
|
627
|
+
* **Promoted from advisory to authoritative (vinaya-milestone-model-v1 task
|
|
628
|
+
* 1): `status` is now `'fail'`, joining A1/A2/A3/M1/M3 as CI-blocking.**
|
|
629
|
+
* Before this task, "the tranche is complete" meant "its Milestone is
|
|
630
|
+
* closed" — a fact a Milestone shared by several tranches can no longer
|
|
631
|
+
* carry reliably (closing it would close every tranche it holds, not just
|
|
632
|
+
* the one that finished). This check's underlying signal was ALREADY the
|
|
633
|
+
* correct one even before this task — per-task-Issue state from
|
|
634
|
+
* `entriesBySlug`, never the Milestone's own open/closed field — so
|
|
635
|
+
* promoting it is a severity change, not a logic change: the signal is now
|
|
636
|
+
* trustworthy enough to be the completeness answer other consumers should
|
|
637
|
+
* defer to, rather than a maybe-stale hint. Applies uniformly to legacy and
|
|
638
|
+
* label-only tranches alike; unlike L4, no legacy restriction is needed here,
|
|
639
|
+
* since Issue state (not Milestone attachment) is what this check reads.
|
|
640
|
+
*
|
|
641
|
+
* Slugs whose facts are unavailable (forge outage) are skipped, mirroring
|
|
642
|
+
* L1's `withFacts.length === 0` guard — an outage is not a finding.
|
|
643
|
+
*/
|
|
644
|
+
export function checkL5(activeTrancheSlugs: string[], entriesBySlug: Map<string, TaskEntry[]>): CheckResult {
|
|
645
|
+
const failures: CheckFailure[] = []
|
|
646
|
+
for (const slug of activeTrancheSlugs) {
|
|
647
|
+
const entries = entriesBySlug.get(slug) ?? []
|
|
648
|
+
const withFacts = entries.filter((e) => e.facts !== undefined)
|
|
649
|
+
if (withFacts.length === 0) continue // forge unavailable or no tasks with issues
|
|
650
|
+
const allClosed = withFacts.every((e) => e.facts?.issueState === 'closed')
|
|
651
|
+
if (allClosed) {
|
|
652
|
+
failures.push({
|
|
653
|
+
tranche: slug,
|
|
654
|
+
reason: 'Every task Issue is closed but the tranche is not recorded as complete — archive it'
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
check: 'L5',
|
|
660
|
+
status: failures.length > 0 ? 'fail' : 'pass',
|
|
661
|
+
failures,
|
|
662
|
+
note:
|
|
663
|
+
failures.length > 0
|
|
664
|
+
? `${failures.length} tranche(s) whose task Issues are all closed but were never archived`
|
|
665
|
+
: undefined
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Extracts the set of Issue numbers a PR body's `Closes #N` (and Fixes/
|
|
670
|
+
* Resolves synonyms) references. Shared by `checkClosesN`'s forward and
|
|
671
|
+
* reverse directions, and by the CI wiring script that must resolve each
|
|
672
|
+
* referenced Issue's task identity *before* calling `checkClosesN` — one
|
|
673
|
+
* grammar, not a second copy of the pattern (discipline). Honors the
|
|
674
|
+
* AEG:CLOSES anchor pair (`anchored-region.ts`, task 30) when present: only
|
|
675
|
+
* references inside the pair count, so a Closes-shaped line in a pasted
|
|
676
|
+
* reference brief elsewhere in the PR body isn't picked up. The searched region
|
|
677
|
+
* is additionally `stripCode`d before matching, for parity with GitHub's
|
|
678
|
+
* auto-close parser (which ignores `Closes #N` inside code) — a backticked-only
|
|
679
|
+
* reference resolves to no Issue here exactly as it does on merge, so this
|
|
680
|
+
* repo-wide check and the pre-merge `checkClosesN` agree with GitHub.
|
|
681
|
+
*
|
|
682
|
+
* The separator groups are bounded (`\s{0,8}`) for the same reason, and to the
|
|
683
|
+
* same width, as `checkClosesN`'s — see the ReDoS note there. The two patterns
|
|
684
|
+
* must stay byte-identical apart from the capture group; a divergence here is
|
|
685
|
+
* a gate-disagreement bug, not a style difference. */
|
|
686
|
+
export function extractClosesReferences(prBody: string): Set<number> {
|
|
687
|
+
const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#(\d+)/gi
|
|
688
|
+
// Strip the WHOLE body, then slice the anchor region out of the stripped
|
|
689
|
+
// text — never strip a sliced region. A region has lost the block context
|
|
690
|
+
// the strip's rules read (list vs indented-code, fence pairing), so an
|
|
691
|
+
// anchor indented inside a list item was blanked as if it were code, and
|
|
692
|
+
// this parser silently disagreed with the whole-body path in the same call
|
|
693
|
+
// (PR #617 review MAJOR). See `archive-task.ts`'s `extractIssue`.
|
|
694
|
+
const stripped = stripCode(prBody)
|
|
695
|
+
const searchIn = anchoredRegion(stripped, 'CLOSES') ?? stripped
|
|
696
|
+
const referenced = new Set<number>()
|
|
697
|
+
for (const hit of searchIn.matchAll(closesPattern)) {
|
|
698
|
+
referenced.add(Number(hit[1]))
|
|
699
|
+
}
|
|
700
|
+
return referenced
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Closes #N gate — Layer 1 of forge-lifecycle enforcement.
|
|
705
|
+
*
|
|
706
|
+
* Forward direction: a task PR (branch `task/<tranche>/<n>`) must carry
|
|
707
|
+
* `Closes #<its-issue>` in the body. Non-task branches are silently
|
|
708
|
+
* bypassed for this direction (returns ok:true).
|
|
709
|
+
*
|
|
710
|
+
* Reverse direction (added: a branch NOT named `task/<tranche>/<n>` that
|
|
711
|
+
* nonetheless closes a real AEG task Issue must be named after that task —
|
|
712
|
+
* the gap that let `feat/vinaya-landing-v3` implement Issue #509 with zero
|
|
713
|
+
* forge-visible status. Runs for ANY branch, gated on `taskIssueRefs` being
|
|
714
|
+
* supplied: each `Closes #N` the body references is looked up in the map;
|
|
715
|
+
* an entry resolving to a task's `{trancheSlug, taskId}` requires
|
|
716
|
+
* `branch === "task/<trancheSlug>/<taskId>"`. A missing map entry (issue not
|
|
717
|
+
* resolved, e.g. no forge token) or an ordinary non-task Issue (`null` in
|
|
718
|
+
* the map) skips the check for that reference — this direction only ever
|
|
719
|
+
* *adds* a failure, never silently passes something the forward direction
|
|
720
|
+
* would have caught.
|
|
721
|
+
*
|
|
722
|
+
* Pure function; reads from injected parameters. The CLI entry-point wires
|
|
723
|
+
* in BRANCH + PR_BODY env vars (forward) and a batched forge lookup
|
|
724
|
+
* (reverse, see `@attalabs/aeg-forge-state`'s `fetchTaskIssueRefs`).
|
|
725
|
+
*/
|
|
726
|
+
export function checkClosesN(
|
|
727
|
+
branch: string,
|
|
728
|
+
prBody: string,
|
|
729
|
+
trancheFiles: TrancheFile[],
|
|
730
|
+
taskIssueRefs?: Map<number, TaskIssueRef | null>
|
|
731
|
+
): { ok: boolean; message?: string; expectedIssue?: number } {
|
|
732
|
+
const referenced = extractClosesReferences(prBody)
|
|
733
|
+
|
|
734
|
+
if (taskIssueRefs) {
|
|
735
|
+
for (const n of referenced) {
|
|
736
|
+
const ref = taskIssueRefs.get(n)
|
|
737
|
+
if (!ref) continue
|
|
738
|
+
const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`
|
|
739
|
+
if (branch !== expectedBranch) {
|
|
740
|
+
return {
|
|
741
|
+
ok: false,
|
|
742
|
+
message: `closes-n-reverse: branch "${branch}" closes #${n} (task ${ref.taskId} of tranche "${ref.trancheSlug}") but is not named "${expectedBranch}" — rename the branch and re-push, or if this work is intentionally outside AEG's dispatch flow, remove the Closes reference.`
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const m = branch.match(/^task\/([^/]+)\/([^/]+)$/)
|
|
749
|
+
if (!m) return { ok: true } // non-task branch — forward direction bypass
|
|
750
|
+
|
|
751
|
+
const trancheSlug = m[1] as string
|
|
752
|
+
const taskId = m[2] as string
|
|
753
|
+
|
|
754
|
+
const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug)
|
|
755
|
+
if (!trancheFile) {
|
|
756
|
+
return {
|
|
757
|
+
ok: false,
|
|
758
|
+
message: `closes-n: branch "${branch}" references tranche "${trancheSlug}" but no topology file found at aeg-root/tranches/${trancheSlug}.md. Ensure the tranche file exists before opening the PR.`
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const task = trancheFile.tranche.tasks.find((t) => t.id === taskId)
|
|
763
|
+
if (!task) {
|
|
764
|
+
return {
|
|
765
|
+
ok: false,
|
|
766
|
+
message: `closes-n: branch "${branch}" references task "${taskId}" not found in ${trancheSlug} topology. Verify the task ID matches the tranche file.`
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (task.issue === null) {
|
|
771
|
+
return {
|
|
772
|
+
ok: false,
|
|
773
|
+
message: `closes-n: task "${taskId}" in "${trancheSlug}" has no Issue number (#TBD). The Planner must cut the Issue before this PR can be validated.`
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const expectedIssue = task.issue
|
|
778
|
+
if (!referenced.has(expectedIssue)) {
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
expectedIssue,
|
|
782
|
+
message: `closes-n: PR body does not contain \`Closes #${expectedIssue}\` (required for task "${taskId}" in tranche "${trancheSlug}"). Add it to the PR body Summary section.`
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { ok: true, expectedIssue }
|
|
787
|
+
}
|