@attalabs/vinaya 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- 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 +26 -20
- 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 +1 -1
- 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 +10 -7
- package/aeg-root/tranche-model.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +552 -139
- package/dist/checks/bin/check-branch-topology.js +603 -144
- package/dist/checks/bin/check-brief-shape.js +593 -142
- package/dist/checks/bin/check-changeset-coverage.js +935 -161
- package/dist/checks/bin/check-closes-n.js +603 -144
- package/dist/checks/bin/check-coherence.js +614 -147
- package/dist/checks/bin/check-dead-branch-push.js +552 -139
- package/dist/checks/bin/check-dispatch-readiness.js +612 -145
- package/dist/checks/bin/check-doc-coverage-push.js +689 -218
- package/dist/checks/bin/check-doc-coverage.js +689 -218
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +684 -146
- package/dist/checks/bin/check-evidence-fresh.js +994 -174
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +603 -144
- package/dist/checks/bin/check-issue-assignment.js +603 -144
- package/dist/checks/bin/check-main-branch-refusal.js +552 -139
- package/dist/checks/bin/check-no-disk-state.js +552 -139
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +621 -155
- package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
- package/dist/checks/bin/check-registry-gates.js +595 -144
- package/dist/checks/bin/check-retired-vocabulary.js +617 -154
- package/dist/checks/bin/check-review-gate.js +670 -141
- package/dist/checks/bin/check-single-plan-pr.js +552 -139
- package/dist/checks/bin/check-test-plan.js +553 -140
- package/dist/checks/bin/check-token-collection-wired.js +552 -139
- package/dist/checks/bin/check-token-report.js +552 -139
- package/dist/checks/bin/check-workspace-escape.js +985 -145
- package/dist/index.js +8500 -5518
- 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]__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]__069-_41._.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/_1fqw88f._.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/node_modules_1vo08dj._.js +2 -2
- 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/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /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/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retired vocabulary — mechanisms genuinely gone from AEG's own methodology
|
|
3
|
+
* (the decision log, the lock, the `team-leader` role, the `D-###` decision
|
|
4
|
+
* id, …) with no legitimate positive occurrence anywhere but the exempt
|
|
5
|
+
* lists below. This is `retired-vocabulary.test.ts`'s `RETIRED` array and
|
|
6
|
+
* the non-citation members of its `RETIRED_IN_PRODUCT` array (both target
|
|
7
|
+
* the same retired-mechanism concern; the split between the two vitest
|
|
8
|
+
* arrays there is organic history, not a semantic boundary) — the file this
|
|
9
|
+
* module was extracted from names the incident: five review rounds each
|
|
10
|
+
* declared the decision-log claim swept, and each time it resurfaced a few
|
|
11
|
+
* lines from where the previous pass fixed it. A rule enforced by attention,
|
|
12
|
+
* not a gate.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately NOT `RETIRED_IN_PRODUCT`'s forge-number / tranche-slug /
|
|
15
|
+
* legacy-slug patterns: those ban a LIVE, unexplained citation, not a
|
|
16
|
+
* retired concept, and are already this package's own
|
|
17
|
+
* `checkUnresolvableReferences` (`reader-resolvable-prose.ts`) — shipping
|
|
18
|
+
* both under two check names would double-report the identical match. This
|
|
19
|
+
* module is deliberately the narrower half of that file's original scan.
|
|
20
|
+
*
|
|
21
|
+
* Content copied verbatim from `retired-vocabulary.test.ts`'s `RETIRED`
|
|
22
|
+
* (minus its citation members), `EXEMPT`, and the non-citation entries of
|
|
23
|
+
* `PATTERN_EXEMPT`, never re-derived — this module's whole purpose is
|
|
24
|
+
* giving that list a CheckSpec adapter, not re-litigating it. Every pattern
|
|
25
|
+
* here is POSIX-ERE-safe (no `\d`/`\w`/`\s`, no lookaheads) by the same
|
|
26
|
+
* discipline `reader-resolvable-prose.ts` documents, so `grep -E` (the
|
|
27
|
+
* vitest suite) and native `RegExp` (this evaluator) agree on every match.
|
|
28
|
+
*
|
|
29
|
+
* Zero I/O: every input (file paths + contents) is read by the adapter and
|
|
30
|
+
* passed in, same charter as `reader-resolvable-prose.ts`.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export type VocabSourceFile = { path: string; content: string }
|
|
34
|
+
|
|
35
|
+
export type VocabFinding = {
|
|
36
|
+
file: string
|
|
37
|
+
line: number
|
|
38
|
+
pattern: string
|
|
39
|
+
message: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The retired mechanisms this scan bans as claims of current-ness — see the module header for what is deliberately NOT here. */
|
|
43
|
+
export const RETIRED_PATTERNS: readonly string[] = [
|
|
44
|
+
// the deleted decision-id format
|
|
45
|
+
String.raw`\bD-[0-9]{3}\b`,
|
|
46
|
+
'D-(###|NNN|nnn|xxx)',
|
|
47
|
+
// the retired record as a live, writable, required artifact, every
|
|
48
|
+
// inflection but never "decision logic"
|
|
49
|
+
'decision (entry|logged)',
|
|
50
|
+
'decision-log entry',
|
|
51
|
+
'decision log entry',
|
|
52
|
+
'decision log(s|ged|ging)?([^a-z]|$)',
|
|
53
|
+
'decision-log',
|
|
54
|
+
'decision entr',
|
|
55
|
+
'decisions? (are |is )?logged',
|
|
56
|
+
'log entries',
|
|
57
|
+
String.raw`decisions\.md`,
|
|
58
|
+
'decisions-legacy',
|
|
59
|
+
'CONTRADICTION',
|
|
60
|
+
'assumes Tier 3',
|
|
61
|
+
// the lock
|
|
62
|
+
'Lock: ?YES',
|
|
63
|
+
'Lock: ?NO',
|
|
64
|
+
'lock approvals?',
|
|
65
|
+
'approves locks',
|
|
66
|
+
'Conforms to lock',
|
|
67
|
+
'Challenges lock',
|
|
68
|
+
// checks deleted with the machinery
|
|
69
|
+
'checkDecisionNumbersFresh',
|
|
70
|
+
// the role that no longer exists
|
|
71
|
+
String.raw`roles/team-leader\.md`,
|
|
72
|
+
// the wreckage a citation strip leaves when it deletes the contents of a
|
|
73
|
+
// parenthetical and not the punctuation around it — the shape is banned,
|
|
74
|
+
// not one instance of it. `()` is deliberately absent: it is an ordinary
|
|
75
|
+
// function call, and banning it would make any TypeScript source unusable.
|
|
76
|
+
String.raw`\(,`,
|
|
77
|
+
String.raw`,\)`,
|
|
78
|
+
String.raw`\(\.\)`
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/** Paths where a retired-vocabulary mention is legitimate: frozen archives and historical records. Substring match, same as the vitest suite's own `EXEMPT`. */
|
|
82
|
+
export const RETIRED_EXEMPT_SUBSTRINGS: readonly string[] = [
|
|
83
|
+
'docs/decisions-legacy.md',
|
|
84
|
+
'apps/herald-ai/docs/',
|
|
85
|
+
'apps/vada-ai/docs/',
|
|
86
|
+
'aeg-root/tranches/completed/',
|
|
87
|
+
'packages/aeg-core/src/docs/published-prose',
|
|
88
|
+
'packages/aeg-core/src/retired-vocabulary.test.ts',
|
|
89
|
+
'packages/aeg-core/src/retired-vocabulary.ts',
|
|
90
|
+
'/fixtures/',
|
|
91
|
+
'/node_modules/',
|
|
92
|
+
'/.next/',
|
|
93
|
+
'/.turbo/'
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Per-pattern exemptions on top of `RETIRED_EXEMPT_SUBSTRINGS`, for the
|
|
98
|
+
* handful of patterns with a real, load-bearing false-positive shape (a
|
|
99
|
+
* live doc that legitimately discusses the retired concept by name, or a
|
|
100
|
+
* same-shaped live feature). Copied verbatim from `retired-vocabulary.test.ts`'s
|
|
101
|
+
* `PATTERN_EXEMPT` — including entries for `apps/herald-ai`/`apps/vada-ai`
|
|
102
|
+
* paths this repo does not carry, since a nonexistent path costs nothing to
|
|
103
|
+
* keep and this module's whole point is not re-deriving the list.
|
|
104
|
+
*/
|
|
105
|
+
export const PATTERN_EXEMPT: Readonly<Record<string, readonly string[]>> = {
|
|
106
|
+
[String.raw`\bD-[0-9]{3}\b`]: ['./CLAUDE.md'],
|
|
107
|
+
[String.raw`decisions\.md`]: [
|
|
108
|
+
'docs-index.md',
|
|
109
|
+
'apps/vada-ai/specs/legacy/README.md',
|
|
110
|
+
'apps/vada-ai/specs/vada-product-spec.md',
|
|
111
|
+
'apps/vada-ai/web/CLAUDE.md',
|
|
112
|
+
'apps/vada-ai/CLAUDE.md'
|
|
113
|
+
],
|
|
114
|
+
'decisions-legacy': [
|
|
115
|
+
'packages/aeg-core/src/file-classify.ts',
|
|
116
|
+
'packages/aeg-core/src/file-classify.test.ts',
|
|
117
|
+
'packages/aeg-core/src/pr-tier.test.ts',
|
|
118
|
+
'docs-index.md',
|
|
119
|
+
'.claude/skills/database/SKILL.md',
|
|
120
|
+
'.claude/skills/vada-architecture/SKILL.md',
|
|
121
|
+
'apps/vada-ai/specs/vada-byok-principles.md',
|
|
122
|
+
'apps/vada-ai/web/CLAUDE.md',
|
|
123
|
+
'apps/vada-ai/CLAUDE.md'
|
|
124
|
+
],
|
|
125
|
+
CONTRADICTION: ['apps/vada-ai/specs/vada-reviewers-spec.md']
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function lineNumbers(content: string, pattern: RegExp): number[] {
|
|
129
|
+
const out: number[] = []
|
|
130
|
+
const lines = content.split('\n')
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
if (pattern.test(lines[i] ?? '')) out.push(i + 1)
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Sweeps `files` for `RETIRED_PATTERNS`, skipping any file whose path
|
|
139
|
+
* contains a `RETIRED_EXEMPT_SUBSTRINGS` entry, or that pattern's own
|
|
140
|
+
* `PATTERN_EXEMPT` entry. Matching is per-line, case-sensitive — the same
|
|
141
|
+
* semantics `grep -E` (with no `-i`) gives the vitest suite this was
|
|
142
|
+
* extracted from.
|
|
143
|
+
*/
|
|
144
|
+
export function scanRetiredVocabulary(files: readonly VocabSourceFile[]): VocabFinding[] {
|
|
145
|
+
const findings: VocabFinding[] = []
|
|
146
|
+
for (const file of files) {
|
|
147
|
+
if (RETIRED_EXEMPT_SUBSTRINGS.some((e) => file.path.includes(e))) continue
|
|
148
|
+
for (const pattern of RETIRED_PATTERNS) {
|
|
149
|
+
const patternExempt = PATTERN_EXEMPT[pattern] ?? []
|
|
150
|
+
if (patternExempt.some((e) => file.path.includes(e))) continue
|
|
151
|
+
const re = new RegExp(pattern)
|
|
152
|
+
for (const line of lineNumbers(file.content, re)) {
|
|
153
|
+
findings.push({
|
|
154
|
+
file: file.path,
|
|
155
|
+
line,
|
|
156
|
+
pattern,
|
|
157
|
+
message: `claims a retired AEG mechanism is still live (matches /${pattern}/)`
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return findings
|
|
163
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Required pre-merge review gate (aeg-review-gate-v1 task 1, #474). Blocks a
|
|
3
|
+
* PR from merging unless a clean code-reviewer `APPROVE` verdict
|
|
4
|
+
* AND a clean security-review `PASS` verdict both exist on the PR — the same
|
|
5
|
+
* `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` detection
|
|
6
|
+
* (`verdict-extraction.ts`) the post-merge Archivist automation already runs,
|
|
7
|
+
* now gated pre-merge and blocking instead of post-merge and advisory-only.
|
|
8
|
+
*
|
|
9
|
+
* Verdict comments are ONLY counted when their author is on the same
|
|
10
|
+
* `PRINCIPAL_ALLOWLIST` the waiver actor-check trusts (security finding,
|
|
11
|
+
* PR #806): body-shape alone is never sufficient on a public repo. Unverified
|
|
12
|
+
* comments are ignored, not fatal.
|
|
13
|
+
*
|
|
14
|
+
* A verified `vinaya/waiver:review` label (the exact actor-verification pattern,
|
|
15
|
+
* `isWaiverLabelActorVerified` reused directly and parameterized by label —
|
|
16
|
+
* see `waiver-label.ts`) lets a principal explicitly skip the requirement for
|
|
17
|
+
* one PR. Label presence alone is never sufficient — only an actor-verified
|
|
18
|
+
* label waives the gate, mirroring exactly.
|
|
19
|
+
*
|
|
20
|
+
* Reviewed-commit binding (#73, a duplicate of #71 closes this one). A clean
|
|
21
|
+
* verdict is no longer sufficient on its own — it must also cover the PR's
|
|
22
|
+
* CURRENT head. Design record (the four questions this fix had to answer,
|
|
23
|
+
* argued in full in the closing PR's body):
|
|
24
|
+
*
|
|
25
|
+
* 1. Commit sha, not tree hash. `headSha` is `gh pr view --json headRefOid`
|
|
26
|
+
* verbatim — one API call, no second fetch of a commit object's tree.
|
|
27
|
+
* Matches the Issue's own proposed fix shape and the hand-typed
|
|
28
|
+
* convention reviewers were already using ("Judged head: <sha>", never a
|
|
29
|
+
* tree hash). Cost accepted: a rebase or an empty amend that leaves the
|
|
30
|
+
* tree byte-identical still invalidates every verdict — no free pass for
|
|
31
|
+
* "the code didn't really change." That is the explicit trade for a
|
|
32
|
+
* single mental model ("a verdict covers an exact commit") over a cheaper
|
|
33
|
+
* but subtler one (two different code states could share a tree).
|
|
34
|
+
* 2. Every push that changes `headRefOid` invalidates unconditionally — a
|
|
35
|
+
* rebase with an identical tree, a merge commit resolving a conflict
|
|
36
|
+
* elsewhere, and a genuine content-changing amend all produce a new sha
|
|
37
|
+
* and therefore a new required verdict. Only a push that does NOT change
|
|
38
|
+
* `headRefOid` at all (a body-only PR-description edit; a force-push that
|
|
39
|
+
* reproduces the exact same commit object) leaves an existing binding
|
|
40
|
+
* intact, because there is nothing for it to have gone stale against.
|
|
41
|
+
* 3. `headSha` is REQUIRED on `ReviewGateInput`, not optional. An optional
|
|
42
|
+
* field that silently skips the binding check when absent is fail-open —
|
|
43
|
+
* exactly the defect class this task exists to close. Every caller
|
|
44
|
+
* (`bin/verify-review-gate.ts`, `apps/cli/src/checks/bin/check-review-gate.ts`)
|
|
45
|
+
* must supply it; the type system enforces that, not a runtime default.
|
|
46
|
+
* 4. Fail-closed for the transition. The moment this merges, every verdict
|
|
47
|
+
* already posted on an open PR is unbound (it carries no `Judged head:`
|
|
48
|
+
* line) and the gate stops honouring it — expensive, and correct: an
|
|
49
|
+
* unbound verdict is exactly the property this task closes. The sanctioned
|
|
50
|
+
* escape for a PR already far along in review is the existing
|
|
51
|
+
* `vinaya/waiver:review` actor-verified label, applied by a principal, the
|
|
52
|
+
* same mechanism that already exists for any other one-off skip.
|
|
53
|
+
*
|
|
54
|
+
* Pure — no `fs`, no `fetch`, no `process.env`. The CLI shim
|
|
55
|
+
* (`bin/verify-review-gate.ts`) resolves the PR's comments/labels/label-actor/
|
|
56
|
+
* head sha via `gh` and calls `checkReviewGate`.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { isPrincipal, isWaiverLabelActorVerified, PRINCIPAL_ALLOWLIST, WAIVER_LABEL_REVIEW } from './waiver-label'
|
|
60
|
+
import { extractCodeReviewVerdict, extractSecurityReviewVerdict } from './verdict-extraction'
|
|
61
|
+
|
|
62
|
+
export type ReviewGateVerdict = 'pass' | 'fail'
|
|
63
|
+
|
|
64
|
+
export type ReviewGateResult = {
|
|
65
|
+
verdict: ReviewGateVerdict
|
|
66
|
+
reason: string
|
|
67
|
+
waived: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ReviewGateComment = {
|
|
71
|
+
body: string
|
|
72
|
+
/** The comment author's GitHub login, or `null` when the caller could not resolve one. */
|
|
73
|
+
author: string | null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ReviewGateInput = {
|
|
77
|
+
/** Every comment on the PR, with its author. */
|
|
78
|
+
comments: ReviewGateComment[]
|
|
79
|
+
/** Every label currently applied to the PR. */
|
|
80
|
+
labels: string[]
|
|
81
|
+
/** Actor of the most recent `vinaya/waiver:review` labeling timeline event, or `null` when none exists. */
|
|
82
|
+
waiverLabelActor: string | null
|
|
83
|
+
/**
|
|
84
|
+
* The PR's current head commit sha (`gh pr view --json headRefOid`),
|
|
85
|
+
* resolved from GitHub — never from local git, an env var, or the PR's own
|
|
86
|
+
* checkout, all three of which a `pull_request`-triggered workflow's
|
|
87
|
+
* PR-editable YAML could steer (#73). REQUIRED, not optional: an omitted
|
|
88
|
+
* head would have to mean either skip-the-binding-check (fail-open, the
|
|
89
|
+
* exact defect this field exists to close) or unconditional-fail, and a
|
|
90
|
+
* required field makes that choice a compile error instead of a runtime
|
|
91
|
+
* default. Every verdict must cover this value to count as clean.
|
|
92
|
+
*/
|
|
93
|
+
headSha: string
|
|
94
|
+
/**
|
|
95
|
+
* Overrides `PRINCIPAL_ALLOWLIST` for this evaluation when provided — an
|
|
96
|
+
* adopter repo's own `vinaya.config.json` `principals` field, resolved by
|
|
97
|
+
* the CLI bin before calling in (never read from here; this stays pure).
|
|
98
|
+
* Defaults to `PRINCIPAL_ALLOWLIST` when omitted, so every existing caller
|
|
99
|
+
* (this repo's own `bin/verify-review-gate.ts` included) is unaffected.
|
|
100
|
+
* `PRINCIPAL_ALLOWLIST` hardcoding this repo's own principal made the gate
|
|
101
|
+
* structurally unpassable on any adopter repo — found live on a real
|
|
102
|
+
* client repo's first dispatched task, the reviewer/security verdicts it
|
|
103
|
+
* already had counted for nobody.
|
|
104
|
+
*/
|
|
105
|
+
principalAllowlist?: string[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Branch names are contributor-controlled metadata, so none can exempt an
|
|
110
|
+
* authority check. The former `plan/*` exemption assumed those branches
|
|
111
|
+
* contained topology docs only; a contributor could put code on a branch
|
|
112
|
+
* with that prefix and make the adapter exit before it fetched the PR. Keep
|
|
113
|
+
* this exported predicate for API compatibility, but fail closed for every
|
|
114
|
+
* branch. A future plan-only exemption would need server-derived changed-file
|
|
115
|
+
* validation, not a name prefix.
|
|
116
|
+
*/
|
|
117
|
+
export function isReviewGateExemptBranch(_branch: string): boolean {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* True when `extraction.headSha` covers `headSha` — an exact match, or
|
|
123
|
+
* `headSha` starting with `extraction.headSha` (the abbreviated-sha case:
|
|
124
|
+
* a verdict may bind against a 7-char prefix, and `headSha` itself is always
|
|
125
|
+
* the full 40-char form GitHub's API returns). `false` when the extraction
|
|
126
|
+
* carries no `headSha` at all (no `Judged head:` line was found) — an
|
|
127
|
+
* unbound verdict never counts as covering anything.
|
|
128
|
+
*/
|
|
129
|
+
function isBoundToHead(extraction: { headSha: string | null }, headSha: string): boolean {
|
|
130
|
+
if (!extraction.headSha) return false
|
|
131
|
+
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `pass` when either (a) `vinaya/waiver:review` is present and actor-verified against
|
|
136
|
+
* `PRINCIPAL_ALLOWLIST`, or (b) both verdicts are clean AND bound — code-reviewer
|
|
137
|
+
* `APPROVE` (not `REQUEST_CHANGES`, not missing, not unclear) covering the PR's
|
|
138
|
+
* current `headSha`, and security-review `PASS` (not `FAIL`, not missing, not
|
|
139
|
+
* unclear) covering it too. `fail` otherwise, naming exactly which verdict(s)
|
|
140
|
+
* are not clean, not bound to the current head, or both.
|
|
141
|
+
*/
|
|
142
|
+
export function checkReviewGate(input: ReviewGateInput): ReviewGateResult {
|
|
143
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST
|
|
144
|
+
const waived = isWaiverLabelActorVerified({
|
|
145
|
+
label: WAIVER_LABEL_REVIEW,
|
|
146
|
+
labels: input.labels,
|
|
147
|
+
labelActor: input.waiverLabelActor,
|
|
148
|
+
principalAllowlist
|
|
149
|
+
})
|
|
150
|
+
if (waived) {
|
|
151
|
+
return {
|
|
152
|
+
verdict: 'pass',
|
|
153
|
+
reason: `\`${WAIVER_LABEL_REVIEW}\` label is actor-verified — review requirement waived for this PR.`,
|
|
154
|
+
waived: true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Verdict-AUTHOR verification (security finding on PR #806): on a public
|
|
159
|
+
// repo any GitHub account can post a `VERDICT: APPROVE`-shaped comment, and
|
|
160
|
+
// most-recent-clear-hit-wins extraction would let a forged later APPROVE
|
|
161
|
+
// override a real earlier REQUEST CHANGES. Only comments whose author is on
|
|
162
|
+
// the same `PRINCIPAL_ALLOWLIST` the waiver's actor check already trusts
|
|
163
|
+
// participate in verdict extraction; everything else — unknown authors and
|
|
164
|
+
// unresolvable (`null`) ones alike — is IGNORED, never fatal, so a drive-by
|
|
165
|
+
// comment cannot brick evaluation, only fail to count. Dispatched reviewer
|
|
166
|
+
// agents post under the principal's own `gh` identity, so the legitimate
|
|
167
|
+
// flow is unchanged.
|
|
168
|
+
const verified = input.comments.filter((c) => isPrincipal(c.author, principalAllowlist))
|
|
169
|
+
// Count only VERDICT-shaped ignored comments — deployment bots and ordinary
|
|
170
|
+
// chat are also non-allowlisted, and counting them would imply forgery
|
|
171
|
+
// where there is only noise (review finding, PR #806).
|
|
172
|
+
const ignoredCount = input.comments.filter(
|
|
173
|
+
(c) => !isPrincipal(c.author, principalAllowlist) && c.body.includes('VERDICT')
|
|
174
|
+
).length
|
|
175
|
+
const verifiedBodies = verified.map((c) => c.body)
|
|
176
|
+
|
|
177
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies)
|
|
178
|
+
const security = extractSecurityReviewVerdict(verifiedBodies)
|
|
179
|
+
const codeReviewClean = codeReview.value === 'APPROVE'
|
|
180
|
+
const securityClean = security.value === 'PASS'
|
|
181
|
+
const codeReviewBound = isBoundToHead(codeReview, input.headSha)
|
|
182
|
+
const securityBound = isBoundToHead(security, input.headSha)
|
|
183
|
+
|
|
184
|
+
if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
|
|
185
|
+
return {
|
|
186
|
+
verdict: 'pass',
|
|
187
|
+
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
|
|
188
|
+
waived: false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const problems: string[] = []
|
|
193
|
+
if (!codeReviewClean) {
|
|
194
|
+
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`)
|
|
195
|
+
} else if (!codeReviewBound) {
|
|
196
|
+
problems.push(
|
|
197
|
+
`the newest code-review verdict covers ${codeReview.headSha ?? 'no recorded commit'}, head is ${input.headSha}`
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
if (!securityClean) {
|
|
201
|
+
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`)
|
|
202
|
+
} else if (!securityBound) {
|
|
203
|
+
problems.push(
|
|
204
|
+
`the newest security-review verdict covers ${security.headSha ?? 'no recorded commit'}, head is ${input.headSha}`
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
const ignoredNote =
|
|
208
|
+
ignoredCount > 0
|
|
209
|
+
? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.`
|
|
210
|
+
: ''
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
verdict: 'fail',
|
|
214
|
+
reason: `${problems.join('; ')}. A principal can apply an actor-verified \`${WAIVER_LABEL_REVIEW}\` label to skip this requirement, or post the missing/clean verdict comment(s).${ignoredNote}`,
|
|
215
|
+
waived: false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Exported so a caller can cheaply pre-check the branch BEFORE paying for
|
|
221
|
+
* whatever it takes to resolve `expectedAuthor` — resolving that value is a
|
|
222
|
+
* network round-trip (`resolveReleaseActor(loadTrustAnchorConfig())`) that
|
|
223
|
+
* must not run on every ordinary PR just because it is one of three
|
|
224
|
+
* arguments to `isChangesetsReleasePr`. Found live (code review, PR #169):
|
|
225
|
+
* evaluating it inline as a function argument runs it unconditionally,
|
|
226
|
+
* regardless of branch, since JS evaluates arguments eagerly.
|
|
227
|
+
*/
|
|
228
|
+
export const CHANGESET_RELEASE_BRANCH = 'changeset-release/main'
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The stock Changesets flow's default identity — a release PR opened by
|
|
232
|
+
* `changesets/action` using the ambient `GITHUB_TOKEN` shows this as its
|
|
233
|
+
* author. Only a SANE DEFAULT for adopters who haven't configured
|
|
234
|
+
* `releaseActor` — never assume it matches any specific repo's real setup.
|
|
235
|
+
* A repo that opens release PRs with a custom PAT (this repo's own
|
|
236
|
+
* `RELEASE_TOKEN`, `.github/workflows/release.yml`) has a DIFFERENT real PR
|
|
237
|
+
* author (the token's owner) and must set `releaseActor` in its
|
|
238
|
+
* `vinaya.config.json`.
|
|
239
|
+
*/
|
|
240
|
+
export const DEFAULT_RELEASE_ACTOR = 'github-actions[bot]'
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* True only for the Changesets release PR. `branch` and `author` must both
|
|
244
|
+
* match — `branch` alone is not a trust boundary, an attacker can push a
|
|
245
|
+
* branch literally called `changeset-release/main`. `expectedAuthor` is
|
|
246
|
+
* caller-resolved, never hardcoded here — this ships in the published
|
|
247
|
+
* `@attalabs/vinaya` package, and hardcoding any one identity would be
|
|
248
|
+
* correct for at most one adopter's release-token setup. Callers resolve it
|
|
249
|
+
* via `resolveReleaseActor(loadTrustAnchorConfig())`, falling back to
|
|
250
|
+
* `DEFAULT_RELEASE_ACTOR`.
|
|
251
|
+
*
|
|
252
|
+
* This predicate is pure and trust-agnostic about ITS OWN inputs — the
|
|
253
|
+
* caller is entirely responsible for where `branch`/`author` come from.
|
|
254
|
+
* `check-body-bare-digits.ts` is the one caller, and only reachable in
|
|
255
|
+
* production from `vinaya-body-checks.yml`'s `pull_request_target` job:
|
|
256
|
+
* `branch`/`author` there come from a live `gh pr view <PR_NUMBER>` fetch,
|
|
257
|
+
* with `PR_NUMBER` itself sourced from `github.event.pull_request.number` —
|
|
258
|
+
* an expression evaluated from the workflow file on the DEFAULT BRANCH, a
|
|
259
|
+
* pull request cannot edit that file to substitute a different literal
|
|
260
|
+
* (the same `pull_request_target` boundary `vinaya-review.yml` already
|
|
261
|
+
* uses). Found live (round 5, PR #165): the identical exemption on a plain
|
|
262
|
+
* `pull_request` trigger let an attacker redirect `PR_NUMBER`/`BRANCH` to
|
|
263
|
+
* any already-approved PR by the configured release actor — verified no
|
|
264
|
+
* env-var or git-state signal inside that trigger type is a safe anchor,
|
|
265
|
+
* which is why this predicate never runs there again.
|
|
266
|
+
*/
|
|
267
|
+
export function isChangesetsReleasePr(branch: string, author: string | null, expectedAuthor: string): boolean {
|
|
268
|
+
return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor
|
|
269
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-plan-PR guard predicate (task 19 / #336). Extracted from
|
|
3
|
+
* `bin/open-pr.ts` (aeg-governance-hardening task 24, #364, Part 1) so the
|
|
4
|
+
* exact same implementation can be consumed by both the ring-0 local wrapper
|
|
5
|
+
* (`open-pr.ts`, prevention) and a ring-1 CI check (detection, for PRs opened
|
|
6
|
+
* via the web UI that bypass the wrapper entirely) — one implementation per
|
|
7
|
+
* fact (§11 constraint), never a second copy of this predicate.
|
|
8
|
+
*
|
|
9
|
+
* Pure — no `fs`, no `gh`/`git` I/O. Callers gather the facts (this branch's
|
|
10
|
+
* touched files, every other open PR's touched files) and pass them in.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parses a tranche slug from a touched file path, when that path is an
|
|
15
|
+
* active (non-`completed/`) tranche topology file. Returns `null` for
|
|
16
|
+
* everything else — including `README.md` and `*.tokens.md`, neither of
|
|
17
|
+
* which is a topology file the single-plan-PR guard (below) cares about.
|
|
18
|
+
*/
|
|
19
|
+
export function trancheSlugFromTopologyPath(path: string): string | null {
|
|
20
|
+
const m = path.match(/^aeg-root\/tranches\/([^/]+)\.md$/)
|
|
21
|
+
if (!m) return null
|
|
22
|
+
const slug = m[1] as string
|
|
23
|
+
if (slug === 'README' || slug.endsWith('.tokens')) return null
|
|
24
|
+
return slug
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type OpenPrFiles = { number: number; files: string[] }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Single-plan-PR guard (task 19 / #336): refuses a plan-branch diff
|
|
31
|
+
* that touches a tranche's topology file when another OPEN PR's diff
|
|
32
|
+
* already touches that SAME tranche's topology file. Ends the plan-PR
|
|
33
|
+
* race that produced two concurrent plan PRs for `aeg-governance-hardening`
|
|
34
|
+
* itself (#352/#354) — each cut from `origin/main` unaware of the other's
|
|
35
|
+
* newly-cut Issue.
|
|
36
|
+
*
|
|
37
|
+
* `branchFiles` is this branch's diff vs `origin/main` (or vs the PR's
|
|
38
|
+
* base); `otherOpenPrs` is every other currently-open PR's touched files
|
|
39
|
+
* (the caller excludes this PR's own number when editing). An ordinary
|
|
40
|
+
* Dormant where plans are forge objects: with a tranche held as a
|
|
41
|
+
* Milestone plus labeled Issues, no plan diff touches a topology file and
|
|
42
|
+
* this predicate has nothing to compare. It stays because it is still the
|
|
43
|
+
* right guard for a repo that keeps plans as files — a dormant check that
|
|
44
|
+
* says so is honest; one that reads as live protection is not.
|
|
45
|
+
*
|
|
46
|
+
* task-branch PR touches no topology file at all, so `branchFiles` yields
|
|
47
|
+
* no slugs and this passes trivially without even needing `otherOpenPrs`.
|
|
48
|
+
*/
|
|
49
|
+
export function checkSinglePlanPr(
|
|
50
|
+
branchFiles: string[],
|
|
51
|
+
otherOpenPrs: OpenPrFiles[]
|
|
52
|
+
): { ok: boolean; message?: string } {
|
|
53
|
+
const touchedSlugs = new Set(branchFiles.map(trancheSlugFromTopologyPath).filter((s): s is string => s !== null))
|
|
54
|
+
if (touchedSlugs.size === 0) return { ok: true }
|
|
55
|
+
|
|
56
|
+
for (const pr of otherOpenPrs) {
|
|
57
|
+
const otherSlugs = new Set(pr.files.map(trancheSlugFromTopologyPath).filter((s): s is string => s !== null))
|
|
58
|
+
for (const slug of touchedSlugs) {
|
|
59
|
+
if (otherSlugs.has(slug)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
message: `single-plan-pr: another open PR (#${pr.number}) already touches tranche "${slug}"'s topology file. Only one open plan PR per tranche is allowed at a time — wait for #${pr.number} to merge or close, or coordinate with its author.`
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { ok: true }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when any of `files` touches an active tranche's topology file — i.e. this diff is a plan-PR diff. */
|
|
71
|
+
export function touchesAnyTopology(files: string[]): boolean {
|
|
72
|
+
return files.some((f) => trancheSlugFromTopologyPath(f) !== null)
|
|
73
|
+
}
|