@attalabs/vinaya 0.3.0 → 0.4.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/aeg-root/enforcement.md +4 -3
- package/aeg-root/glossary.md +4 -0
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/roles/security.md +1 -1
- package/dist/checks/bin/check-branch-topology.js +7 -3
- package/dist/checks/bin/check-brief-shape.js +7 -3
- package/dist/checks/bin/check-closes-n.js +7 -3
- package/dist/checks/bin/check-coherence.js +7 -3
- package/dist/checks/bin/check-dead-branch-push.js +7 -3
- package/dist/checks/bin/check-dispatch-readiness.js +7 -3
- package/dist/checks/bin/check-doc-coverage-push.js +7 -3
- package/dist/checks/bin/check-doc-coverage.js +7 -3
- package/dist/checks/bin/check-first-push-dispatch.js +7 -3
- package/dist/checks/bin/check-issue-assignment.js +7 -3
- package/dist/checks/bin/check-no-disk-state.js +7 -3
- package/dist/checks/bin/check-reader-resolvable-prose.js +7 -3
- package/dist/checks/bin/check-registry-gates.js +7 -3
- package/dist/checks/bin/check-review-gate.js +12 -4
- package/dist/checks/bin/check-single-plan-pr.js +7 -3
- package/dist/checks/bin/check-test-plan.js +7 -3
- package/dist/index.js +324 -31
- package/package.json +1 -1
package/aeg-root/enforcement.md
CHANGED
|
@@ -44,7 +44,8 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
44
44
|
| Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. | **`verify-dispatch` CLI** (`packages/aeg-core/bin/verify-dispatch.ts` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **`.husky/pre-push`** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/verify-dispatch.ts` |
|
|
45
45
|
| Opening a task PR whose surface includes real code | Ever had a PR's description quietly stop matching what the code does? | event | Refuses a code-carrying pull request whose description no longer matches what it changes. | **Brief Validation — `checkPremiseCoverage`** | The PR body carries a `Premise:` block with at least one assertion (`contains`/`absent`/`sha256`) whose path falls inside the task's surface map — re-assertable at any point via `verify-dispatch --premise` or `verify-task`. | `packages/aeg-core/src/brief-validation.ts` |
|
|
46
46
|
| Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | `packages/aeg-core/bin/verify-task.ts` |
|
|
47
|
-
| Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/vinaya/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/vinaya/cli/src/checks/runner.ts`) | **⚠️ BREAKING, live (task 3):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) explicitly forwards — **no longer the full parent environment.** An undeclared variable a check's own code reads is now genuinely invisible to it, not merely warned about; a `true` or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` and the check never spawns at all. All 15 core checks in `registry.ts` carry an audited declaration, so
|
|
47
|
+
| Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/vinaya/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/vinaya/cli/src/checks/runner.ts`) | **⚠️ BREAKING, live (task 3):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) explicitly forwards — **no longer the full parent environment.** An undeclared variable a check's own code reads is now genuinely invisible to it, not merely warned about; a `true` or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` and the check never spawns at all. All 15 core checks in `registry.ts` carry an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/vinaya/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations). This repo's own core checks are therefore unaffected — an adopter's **custom** check that reads `process.env` directly with no `env` declared will lose that access at this minor; declare `env` on it (`vinaya.config.json`'s `checks.<name>.env`) before upgrading. `vinaya check`'s prior warn-phase print (task 2) is retired now that the behavior it warned about is live; `vinaya doctor` still carries the same missing-declaration diagnostic permanently, at `info` severity, so a custom check's gap remains visible after upgrade even though `vinaya check` no longer prints it inline. | `apps/vinaya/cli/src/checks/runner.ts` |
|
|
48
|
+
| `vinaya check` / `vinaya check --plan` (a CLI mechanism, not a git hook — this row is `.husky`-free) | Ever had a config entry silently double-run alongside the core check it was meant to replace, with no way to see that from the outside? | event | Resolves core-registered and config-registered checks into one deterministic table before anything runs, instead of letting a config entry run alongside the core check it collides with, unannounced. | **Checks-side resolver** (`resolveChecks`/`isValidNamespacedKey`, `apps/vinaya/cli/src/checks/resolver.ts`) | An exact-key match against a core check ID is an **override** — a complete replacement of the spec, never a merge. A key containing exactly one `/`, both segments matching `[a-z0-9][a-z0-9-]*` and the `vinaya` prefix rejected as an exact segment match, is **additive**. Anything else — a bare key with no `/` matching no core ID — is a loud `FAIL_CLOSED` refusal: never silently dropped, never defaulted, never last-wins. `--plan` never resolves or prints an env *value*, only how each one resolves. **This release feeds `vinaya check --plan` / `--plan --json` only** — `vinaya check`'s real execution (`check.ts`'s flat `[...coreCheckRegistry(), ...customSpecs]` concat) is unaffected until a later task wires it in. | `apps/vinaya/cli/src/checks/resolver.ts` |
|
|
48
49
|
|
|
49
50
|
**Documentation coverage, specifically** (a historical pain point): the code→document ownership rule is enforced at *two* prevention chokepoints — at every push (over the branch's cumulative change set) and again at pull-request creation and editing. A change to owned code cannot be published, let alone turned into a pull request, without its owning document.
|
|
50
51
|
|
|
@@ -72,8 +73,8 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
72
73
|
| Test-plan state | Ever merged a PR with an unchecked "did you test this" box? | ci | Re-checks that the pull request’s test plan is genuinely ticked. | Unticked test-plan boxes block merge readiness | `packages/aeg-core/bin/verify-test-plan.ts` |
|
|
73
74
|
| Typecheck + unit tests | Ever had a change silently break something it wasn't even touching? | ci | Re-runs the type checker and the unit tests for every package this change can reach. | Packages affected by this PR's diff, plus their full transitive-dependent set (`turbo --affected`; application builds are verified by the deployment pipeline). A diff touching no package (docs/workflow-only) runs 0 packages — sound, since repo-file gates like `verify-docs`/coherence run in their own jobs regardless (task 27) | `.github/workflows/ci.yml` |
|
|
74
75
|
| Conventions | Ever opened a PR full of inconsistent formatting and naming? | ci | Re-checks formatting and naming against the repo’s own conventions. | Lint/format, commit-message grammar, no hardcoded UI colors | `.github/workflows/forge-lifecycle.yml` |
|
|
75
|
-
| AI review | Ever wished every PR got a second pair of eyes, even at 2am? | ci | Puts an automated reviewer on every pull request, whatever the hour. | Posts a judgment review of the change.
|
|
76
|
-
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. | `packages/aeg-core/bin/verify-review-gate.ts` |
|
|
76
|
+
| AI review | Ever wished every PR got a second pair of eyes, even at 2am? | ci | Puts an automated reviewer on every pull request, whatever the hour. | Posts a judgment review of the change. **Advisory only, including its verdict markers** (ruled 2026-08-09, verdict-author verification): the Review gate below counts a verdict comment only when its author is on the principal allowlist, and this job posts under a bot identity — deliberately excluded, because the job reads the PR's own content, so a crafted PR can steer what it writes; letting its `APPROVE` gate merges would re-open the forged-verdict channel author verification closes. In practice this changes nothing: the gate has only ever flipped on principal-identity verdicts. The bot's review remains visible input to the human/dispatched reviews | `.github/workflows/claude-code-review.yml` |
|
|
77
|
+
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. | `packages/aeg-core/bin/verify-review-gate.ts` |
|
|
77
78
|
| G1 — implementation exists | Ever read about a safeguard that turned out not to actually exist? | ci | Re-checks that every gate the doctrine describes has real code behind it. | Every ring's non-empty `implementation` cell on this very page resolves to a real path on disk — makes this page's registry columns load-bearing instead of decorative. | `packages/aeg-core/bin/verify-registry.ts` |
|
|
78
79
|
| G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts` is named by some row's `implementation` — the inverse direction of G1: no enforcement mechanism exists off-page. | `packages/aeg-core/bin/verify-registry.ts` |
|
|
79
80
|
| G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making the exact class of GitHub-mutating call `.claude/hooks/check-forge-gates.sh` itself gates (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. | `packages/aeg-core/bin/verify-registry.ts` |
|
package/aeg-root/glossary.md
CHANGED
|
@@ -21,6 +21,10 @@ Vinaya uses a handful of words in a specific, non-obvious sense — on the pages
|
|
|
21
21
|
|
|
22
22
|
**Ratification** — Sign-off, from the person ultimately accountable for the product, on a decision or a piece of work that would be hard to undo. Higher-impact changes wait for a scheduled sign-off window rather than merging the moment they're technically ready.
|
|
23
23
|
|
|
24
|
+
**Registry ID / rendering ID** — Two names for the same role that can differ. The registry ID is the config key an adopter uses to claim or add a role — namespaced for an additive one. The rendering ID is the role file's own `role_id`, always slash-free, what every diagram and downstream reader actually sees. An override's rendering ID must equal its registry ID exactly; an additive role's rendering ID must equal the key's segment after the `/`.
|
|
25
|
+
|
|
26
|
+
**Resolution state** — One of three states — `default`, `overridden`, or `additive` — that every check or role ID resolves to. `default` means shipped-and-unmodified; `overridden` means a config entry currently claims that ID and satisfies its contract; `additive` means a wholly new, namespaced entry the shipped product has no opinion on.
|
|
27
|
+
|
|
24
28
|
**Seam** — The handoff point between two people or roles, where one produces something — a document, a decision, a record — and the other consumes it. Each seam is written down once, in its own place, so both sides can agree on exactly what crosses it.
|
|
25
29
|
|
|
26
30
|
**Step 0** — The literal first command someone runs before touching any code on a task: the one that creates that task's own isolated copy of the repository and its own branch. Nothing else happens until this command has run.
|
|
@@ -123,7 +123,7 @@ If you discover something that needs a decision above review authority — the b
|
|
|
123
123
|
|
|
124
124
|
Phase 10 (Review) in `process.md`. The order is: **code-reviewer pass (you) → security pass (`roles/security.md`) → Principal code review → Brief Author spec review → merge.** Your verdict feeds the human reviews; it does not replace them.
|
|
125
125
|
|
|
126
|
-
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment for a clean `APPROVE` verdict — `REQUEST CHANGES`, a missing verdict, or an unclear one all fail the check and block merge, same as this repo's own security pass. This is not advisory: it is the same enforcement class as typecheck or lint. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (`aeg-root/enforcement.md`) — label presence alone is never sufficient.
|
|
126
|
+
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `APPROVE` verdict — `REQUEST CHANGES`, a missing verdict, or an unclear one all fail the check and block merge, same as this repo's own security pass. This is not advisory: it is the same enforcement class as typecheck or lint. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (`aeg-root/enforcement.md`) — label presence alone is never sufficient.
|
|
127
127
|
|
|
128
128
|
## Turn-end: report your tokens in the verdict comment
|
|
129
129
|
|
|
@@ -114,7 +114,7 @@ A security finding that implies a product/architecture decision (e.g., "the whol
|
|
|
114
114
|
|
|
115
115
|
Phase 10 (Review) in `process.md`: code-reviewer pass → **security pass (you)** → Principal code review → Brief Author spec review → merge.
|
|
116
116
|
|
|
117
|
-
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment for a clean `PASS` verdict — `FAIL`, a missing verdict, or an unclear one all fail the check and block merge, same as the code-reviewer pass. This is not advisory: it is the same enforcement class as typecheck or lint. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (`aeg-root/enforcement.md`) — label presence alone is never sufficient.
|
|
117
|
+
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `PASS` verdict — `FAIL`, a missing verdict, or an unclear one all fail the check and block merge, same as the code-reviewer pass. This is not advisory: it is the same enforcement class as typecheck or lint. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (`aeg-root/enforcement.md`) — label presence alone is never sufficient.
|
|
118
118
|
|
|
119
119
|
## Turn-end: report your tokens in the verdict comment
|
|
120
120
|
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2167,8 +2167,11 @@ function checkReviewGate(input) {
|
|
|
2167
2167
|
waived: true
|
|
2168
2168
|
};
|
|
2169
2169
|
}
|
|
2170
|
-
const
|
|
2171
|
-
const
|
|
2170
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2171
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2172
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2173
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2174
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2172
2175
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2173
2176
|
const securityClean = security.value === "PASS";
|
|
2174
2177
|
if (codeReviewClean && securityClean) {
|
|
@@ -2183,9 +2186,10 @@ function checkReviewGate(input) {
|
|
|
2183
2186
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2184
2187
|
if (!securityClean)
|
|
2185
2188
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2189
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2186
2190
|
return {
|
|
2187
2191
|
verdict: "fail",
|
|
2188
|
-
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)
|
|
2192
|
+
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}`,
|
|
2189
2193
|
waived: false
|
|
2190
2194
|
};
|
|
2191
2195
|
}
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2171,8 +2171,11 @@ function checkReviewGate(input) {
|
|
|
2171
2171
|
waived: true
|
|
2172
2172
|
};
|
|
2173
2173
|
}
|
|
2174
|
-
const
|
|
2175
|
-
const
|
|
2174
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2175
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2176
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2177
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2178
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2176
2179
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2177
2180
|
const securityClean = security.value === "PASS";
|
|
2178
2181
|
if (codeReviewClean && securityClean) {
|
|
@@ -2187,9 +2190,10 @@ function checkReviewGate(input) {
|
|
|
2187
2190
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2188
2191
|
if (!securityClean)
|
|
2189
2192
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2193
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2190
2194
|
return {
|
|
2191
2195
|
verdict: "fail",
|
|
2192
|
-
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)
|
|
2196
|
+
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}`,
|
|
2193
2197
|
waived: false
|
|
2194
2198
|
};
|
|
2195
2199
|
}
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2171,8 +2171,11 @@ function checkReviewGate(input) {
|
|
|
2171
2171
|
waived: true
|
|
2172
2172
|
};
|
|
2173
2173
|
}
|
|
2174
|
-
const
|
|
2175
|
-
const
|
|
2174
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2175
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2176
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2177
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2178
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2176
2179
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2177
2180
|
const securityClean = security.value === "PASS";
|
|
2178
2181
|
if (codeReviewClean && securityClean) {
|
|
@@ -2187,9 +2190,10 @@ function checkReviewGate(input) {
|
|
|
2187
2190
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2188
2191
|
if (!securityClean)
|
|
2189
2192
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2193
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2190
2194
|
return {
|
|
2191
2195
|
verdict: "fail",
|
|
2192
|
-
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)
|
|
2196
|
+
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}`,
|
|
2193
2197
|
waived: false
|
|
2194
2198
|
};
|
|
2195
2199
|
}
|
|
@@ -2171,8 +2171,11 @@ function checkReviewGate(input) {
|
|
|
2171
2171
|
waived: true
|
|
2172
2172
|
};
|
|
2173
2173
|
}
|
|
2174
|
-
const
|
|
2175
|
-
const
|
|
2174
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2175
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2176
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2177
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2178
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2176
2179
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2177
2180
|
const securityClean = security.value === "PASS";
|
|
2178
2181
|
if (codeReviewClean && securityClean) {
|
|
@@ -2187,9 +2190,10 @@ function checkReviewGate(input) {
|
|
|
2187
2190
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2188
2191
|
if (!securityClean)
|
|
2189
2192
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2193
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2190
2194
|
return {
|
|
2191
2195
|
verdict: "fail",
|
|
2192
|
-
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)
|
|
2196
|
+
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}`,
|
|
2193
2197
|
waived: false
|
|
2194
2198
|
};
|
|
2195
2199
|
}
|
|
@@ -2172,8 +2172,11 @@ function checkReviewGate(input) {
|
|
|
2172
2172
|
waived: true
|
|
2173
2173
|
};
|
|
2174
2174
|
}
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2175
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2176
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2177
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2178
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2179
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2177
2180
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2178
2181
|
const securityClean = security.value === "PASS";
|
|
2179
2182
|
if (codeReviewClean && securityClean) {
|
|
@@ -2188,9 +2191,10 @@ function checkReviewGate(input) {
|
|
|
2188
2191
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2189
2192
|
if (!securityClean)
|
|
2190
2193
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2194
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2191
2195
|
return {
|
|
2192
2196
|
verdict: "fail",
|
|
2193
|
-
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)
|
|
2197
|
+
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}`,
|
|
2194
2198
|
waived: false
|
|
2195
2199
|
};
|
|
2196
2200
|
}
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2172,8 +2172,11 @@ function checkReviewGate(input) {
|
|
|
2172
2172
|
waived: true
|
|
2173
2173
|
};
|
|
2174
2174
|
}
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2175
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2176
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2177
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2178
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2179
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2177
2180
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2178
2181
|
const securityClean = security.value === "PASS";
|
|
2179
2182
|
if (codeReviewClean && securityClean) {
|
|
@@ -2188,9 +2191,10 @@ function checkReviewGate(input) {
|
|
|
2188
2191
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2189
2192
|
if (!securityClean)
|
|
2190
2193
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2194
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2191
2195
|
return {
|
|
2192
2196
|
verdict: "fail",
|
|
2193
|
-
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)
|
|
2197
|
+
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}`,
|
|
2194
2198
|
waived: false
|
|
2195
2199
|
};
|
|
2196
2200
|
}
|
|
@@ -2172,8 +2172,11 @@ function checkReviewGate(input) {
|
|
|
2172
2172
|
waived: true
|
|
2173
2173
|
};
|
|
2174
2174
|
}
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2175
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2176
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2177
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2178
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2179
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2177
2180
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2178
2181
|
const securityClean = security.value === "PASS";
|
|
2179
2182
|
if (codeReviewClean && securityClean) {
|
|
@@ -2188,9 +2191,10 @@ function checkReviewGate(input) {
|
|
|
2188
2191
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2189
2192
|
if (!securityClean)
|
|
2190
2193
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2194
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2191
2195
|
return {
|
|
2192
2196
|
verdict: "fail",
|
|
2193
|
-
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)
|
|
2197
|
+
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}`,
|
|
2194
2198
|
waived: false
|
|
2195
2199
|
};
|
|
2196
2200
|
}
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2868,7 +2872,11 @@ function main() {
|
|
|
2868
2872
|
}
|
|
2869
2873
|
const labels = pr.labels.map((l) => l.name);
|
|
2870
2874
|
const waiverLabelActor = labels.includes(WAIVER_LABEL_REVIEW) ? fetchWaiverLabelActor(prNumber, WAIVER_LABEL_REVIEW) : null;
|
|
2871
|
-
const result = checkReviewGate({
|
|
2875
|
+
const result = checkReviewGate({
|
|
2876
|
+
comments: pr.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null })),
|
|
2877
|
+
labels,
|
|
2878
|
+
waiverLabelActor
|
|
2879
|
+
});
|
|
2872
2880
|
if (result.verdict === "fail") {
|
|
2873
2881
|
emitCheckError({
|
|
2874
2882
|
schema: CHECK_SCHEMA_VERSION,
|
|
@@ -2170,8 +2170,11 @@ function checkReviewGate(input) {
|
|
|
2170
2170
|
waived: true
|
|
2171
2171
|
};
|
|
2172
2172
|
}
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2173
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2174
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2175
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2176
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2177
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2175
2178
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2176
2179
|
const securityClean = security.value === "PASS";
|
|
2177
2180
|
if (codeReviewClean && securityClean) {
|
|
@@ -2186,9 +2189,10 @@ function checkReviewGate(input) {
|
|
|
2186
2189
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2187
2190
|
if (!securityClean)
|
|
2188
2191
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2192
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2189
2193
|
return {
|
|
2190
2194
|
verdict: "fail",
|
|
2191
|
-
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)
|
|
2195
|
+
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}`,
|
|
2192
2196
|
waived: false
|
|
2193
2197
|
};
|
|
2194
2198
|
}
|
|
@@ -2167,8 +2167,11 @@ function checkReviewGate(input) {
|
|
|
2167
2167
|
waived: true
|
|
2168
2168
|
};
|
|
2169
2169
|
}
|
|
2170
|
-
const
|
|
2171
|
-
const
|
|
2170
|
+
const verified = input.comments.filter((c) => c.author !== null && PRINCIPAL_ALLOWLIST.includes(c.author));
|
|
2171
|
+
const ignoredCount = input.comments.filter((c) => (c.author === null || !PRINCIPAL_ALLOWLIST.includes(c.author)) && c.body.includes("VERDICT")).length;
|
|
2172
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2173
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2174
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2172
2175
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2173
2176
|
const securityClean = security.value === "PASS";
|
|
2174
2177
|
if (codeReviewClean && securityClean) {
|
|
@@ -2183,9 +2186,10 @@ function checkReviewGate(input) {
|
|
|
2183
2186
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2184
2187
|
if (!securityClean)
|
|
2185
2188
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2189
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2186
2190
|
return {
|
|
2187
2191
|
verdict: "fail",
|
|
2188
|
-
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)
|
|
2192
|
+
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}`,
|
|
2189
2193
|
waived: false
|
|
2190
2194
|
};
|
|
2191
2195
|
}
|
package/dist/index.js
CHANGED
|
@@ -1334,7 +1334,9 @@ function coreCheckRegistry() {
|
|
|
1334
1334
|
timeoutMs: 15000,
|
|
1335
1335
|
env: {
|
|
1336
1336
|
BASE_SHA: { optional: true },
|
|
1337
|
-
PR_NUMBER: { optional: true }
|
|
1337
|
+
PR_NUMBER: { optional: true },
|
|
1338
|
+
GITHUB_TOKEN: { optional: true },
|
|
1339
|
+
GH_TOKEN: { optional: true }
|
|
1338
1340
|
}
|
|
1339
1341
|
},
|
|
1340
1342
|
{
|
|
@@ -1359,7 +1361,11 @@ function coreCheckRegistry() {
|
|
|
1359
1361
|
run: bin("check-registry-gates"),
|
|
1360
1362
|
scope: "full",
|
|
1361
1363
|
timeoutMs: 30000,
|
|
1362
|
-
env: {
|
|
1364
|
+
env: {
|
|
1365
|
+
AEG_REPO: { optional: true },
|
|
1366
|
+
GITHUB_TOKEN: { optional: true },
|
|
1367
|
+
GH_TOKEN: { optional: true }
|
|
1368
|
+
}
|
|
1363
1369
|
},
|
|
1364
1370
|
{
|
|
1365
1371
|
name: "review-gate",
|
|
@@ -1368,7 +1374,9 @@ function coreCheckRegistry() {
|
|
|
1368
1374
|
timeoutMs: 30000,
|
|
1369
1375
|
env: {
|
|
1370
1376
|
BRANCH: { optional: true },
|
|
1371
|
-
PR_NUMBER: { optional: true }
|
|
1377
|
+
PR_NUMBER: { optional: true },
|
|
1378
|
+
GITHUB_TOKEN: { optional: true },
|
|
1379
|
+
GH_TOKEN: { optional: true }
|
|
1372
1380
|
}
|
|
1373
1381
|
},
|
|
1374
1382
|
{
|
|
@@ -1378,7 +1386,9 @@ function coreCheckRegistry() {
|
|
|
1378
1386
|
timeoutMs: 30000,
|
|
1379
1387
|
env: {
|
|
1380
1388
|
BRANCH: { optional: true },
|
|
1381
|
-
AEG_REPO: { optional: true }
|
|
1389
|
+
AEG_REPO: { optional: true },
|
|
1390
|
+
GITHUB_TOKEN: { optional: true },
|
|
1391
|
+
GH_TOKEN: { optional: true }
|
|
1382
1392
|
}
|
|
1383
1393
|
},
|
|
1384
1394
|
{
|
|
@@ -1386,7 +1396,11 @@ function coreCheckRegistry() {
|
|
|
1386
1396
|
run: bin("check-dead-branch-push"),
|
|
1387
1397
|
scope: "full",
|
|
1388
1398
|
timeoutMs: 30000,
|
|
1389
|
-
env: {
|
|
1399
|
+
env: {
|
|
1400
|
+
BRANCH: { optional: true },
|
|
1401
|
+
GITHUB_TOKEN: { optional: true },
|
|
1402
|
+
GH_TOKEN: { optional: true }
|
|
1403
|
+
}
|
|
1390
1404
|
},
|
|
1391
1405
|
{
|
|
1392
1406
|
name: "first-push-dispatch",
|
|
@@ -1421,12 +1435,62 @@ function coreCheckRegistry() {
|
|
|
1421
1435
|
timeoutMs: 30000,
|
|
1422
1436
|
env: {
|
|
1423
1437
|
AEG_REPO: { optional: true },
|
|
1424
|
-
BRANCH: { optional: true }
|
|
1438
|
+
BRANCH: { optional: true },
|
|
1439
|
+
GITHUB_TOKEN: { optional: true },
|
|
1440
|
+
GH_TOKEN: { optional: true }
|
|
1425
1441
|
}
|
|
1426
1442
|
}
|
|
1427
1443
|
];
|
|
1428
1444
|
}
|
|
1429
1445
|
|
|
1446
|
+
// src/checks/resolver.ts
|
|
1447
|
+
var NAMESPACE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
|
|
1448
|
+
var RESERVED_PREFIX = "vinaya";
|
|
1449
|
+
function isValidNamespacedKey(key) {
|
|
1450
|
+
const parts = key.split("/");
|
|
1451
|
+
if (parts.length !== 2)
|
|
1452
|
+
return false;
|
|
1453
|
+
const [prefix, rest] = parts;
|
|
1454
|
+
if (!prefix || !rest)
|
|
1455
|
+
return false;
|
|
1456
|
+
if (!NAMESPACE_SEGMENT.test(prefix) || !NAMESPACE_SEGMENT.test(rest))
|
|
1457
|
+
return false;
|
|
1458
|
+
if (prefix === RESERVED_PREFIX)
|
|
1459
|
+
return false;
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
function resolveChecks(core, configChecks) {
|
|
1463
|
+
const resolved = core.map((spec) => ({ name: spec.name, state: "default", source: "core", spec }));
|
|
1464
|
+
const failures = [];
|
|
1465
|
+
if (!configChecks)
|
|
1466
|
+
return { resolved, failures };
|
|
1467
|
+
const indexByName = new Map(resolved.map((entry2, index) => [entry2.name, index]));
|
|
1468
|
+
for (const [key, entry2] of Object.entries(configChecks)) {
|
|
1469
|
+
const coreIndex = indexByName.get(key);
|
|
1470
|
+
if (coreIndex !== undefined) {
|
|
1471
|
+
resolved[coreIndex] = {
|
|
1472
|
+
name: key,
|
|
1473
|
+
state: "overridden",
|
|
1474
|
+
source: "config",
|
|
1475
|
+
spec: { name: key, ...entry2 }
|
|
1476
|
+
};
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (isValidNamespacedKey(key)) {
|
|
1480
|
+
resolved.push({ name: key, state: "additive", source: "config", spec: { name: key, ...entry2 } });
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
failures.push({ key, reason: 'bare key has no "/" and matches no core check id' });
|
|
1484
|
+
}
|
|
1485
|
+
return { resolved, failures };
|
|
1486
|
+
}
|
|
1487
|
+
function overriddenNextMinorWarning(name) {
|
|
1488
|
+
return `check "${name}" shares its name with a core check — it will replace the core check starting next minor (currently it runs alongside it under the flat registry).`;
|
|
1489
|
+
}
|
|
1490
|
+
function bareKeyNextMinorWarning(key) {
|
|
1491
|
+
return `check "${key}" has no namespace and matches no core check — it will be rejected starting next minor. Rename it to "<yourname>/x".`;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1430
1494
|
// src/checks/runner.ts
|
|
1431
1495
|
import { spawn } from "node:child_process";
|
|
1432
1496
|
import { cpus } from "node:os";
|
|
@@ -1832,6 +1896,15 @@ function configPath() {
|
|
|
1832
1896
|
return GLOBAL_CONFIG_PATH;
|
|
1833
1897
|
return null;
|
|
1834
1898
|
}
|
|
1899
|
+
function globalChecksIgnoredWarning(path) {
|
|
1900
|
+
return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
|
|
1901
|
+
}
|
|
1902
|
+
function stripGlobalChecks(config, path) {
|
|
1903
|
+
if (path !== GLOBAL_CONFIG_PATH || !config.checks || Object.keys(config.checks).length === 0)
|
|
1904
|
+
return config;
|
|
1905
|
+
console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
|
|
1906
|
+
return { ...config, checks: undefined };
|
|
1907
|
+
}
|
|
1835
1908
|
function loadConfigChecked() {
|
|
1836
1909
|
const path = configPath();
|
|
1837
1910
|
if (!path)
|
|
@@ -1847,7 +1920,7 @@ function loadConfigChecked() {
|
|
|
1847
1920
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
1848
1921
|
return { ok: false, path, error: detail };
|
|
1849
1922
|
}
|
|
1850
|
-
return { ok: true, config: parsed.data };
|
|
1923
|
+
return { ok: true, config: stripGlobalChecks(parsed.data, path) };
|
|
1851
1924
|
}
|
|
1852
1925
|
|
|
1853
1926
|
// src/commands/check.ts
|
|
@@ -1887,25 +1960,112 @@ function configErrorOutcome(path, error) {
|
|
|
1887
1960
|
function customSpecsFromConfig() {
|
|
1888
1961
|
const result = loadConfigChecked();
|
|
1889
1962
|
if (!result.ok)
|
|
1890
|
-
return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error) };
|
|
1963
|
+
return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error), checks: undefined };
|
|
1891
1964
|
const checks = result.config?.checks;
|
|
1892
1965
|
if (!checks)
|
|
1893
|
-
return { specs: [], errorOutcome: null };
|
|
1966
|
+
return { specs: [], errorOutcome: null, checks: undefined };
|
|
1894
1967
|
const specs = Object.entries(checks).map(([name, entry2]) => ({ name, ...entry2 }));
|
|
1895
|
-
return { specs, errorOutcome: null };
|
|
1968
|
+
return { specs, errorOutcome: null, checks };
|
|
1969
|
+
}
|
|
1970
|
+
function classifyEnvValue(value) {
|
|
1971
|
+
if (value === true)
|
|
1972
|
+
return "passthrough";
|
|
1973
|
+
if (typeof value === "string")
|
|
1974
|
+
return "literal";
|
|
1975
|
+
if ("anyOf" in value)
|
|
1976
|
+
return "anyOf";
|
|
1977
|
+
return "optional";
|
|
1978
|
+
}
|
|
1979
|
+
function renderPlanJson(result) {
|
|
1980
|
+
const checks = {};
|
|
1981
|
+
for (const entry2 of result.resolved) {
|
|
1982
|
+
const env = {};
|
|
1983
|
+
let envAnyOf;
|
|
1984
|
+
for (const [key, value] of Object.entries(entry2.spec.env ?? {})) {
|
|
1985
|
+
env[key] = classifyEnvValue(value);
|
|
1986
|
+
if (typeof value === "object" && value !== null && "anyOf" in value) {
|
|
1987
|
+
envAnyOf ??= {};
|
|
1988
|
+
envAnyOf[key] = value.anyOf;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
checks[entry2.name] = { state: entry2.state, source: entry2.source, env, envAnyOf, scope: entry2.spec.scope };
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
schema: 1,
|
|
1995
|
+
checks,
|
|
1996
|
+
roles: { available: false, reason: "role resolution not implemented yet — see task 6" },
|
|
1997
|
+
errors: result.failures
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
function envCellFor(spec) {
|
|
2001
|
+
const entries = Object.entries(spec.env ?? {});
|
|
2002
|
+
if (entries.length === 0)
|
|
2003
|
+
return "—";
|
|
2004
|
+
return entries.map(([key, value]) => {
|
|
2005
|
+
if (value === true)
|
|
2006
|
+
return key;
|
|
2007
|
+
if (typeof value === "string")
|
|
2008
|
+
return `${key}=<redacted>`;
|
|
2009
|
+
if ("anyOf" in value)
|
|
2010
|
+
return value.anyOf.join("|");
|
|
2011
|
+
return `${key}?`;
|
|
2012
|
+
}).join(", ");
|
|
2013
|
+
}
|
|
2014
|
+
function renderPlanTable(result) {
|
|
2015
|
+
const header = ["NAME", "STATE", "SOURCE", "ENV"];
|
|
2016
|
+
const rows = result.resolved.map((entry2) => [entry2.name, entry2.state, entry2.source, envCellFor(entry2.spec)]);
|
|
2017
|
+
const failureRows = result.failures.map((f) => [f.key, "FAILED", "—", f.reason]);
|
|
2018
|
+
const table = [header, ...rows, ...failureRows];
|
|
2019
|
+
const widths = header.map((_, col) => Math.max(...table.map((row) => row[col]?.length ?? 0)));
|
|
2020
|
+
return table.map((row) => row.map((cell, col) => (cell ?? "").padEnd(widths[col] ?? 0)).join(" ")).join(`
|
|
2021
|
+
`);
|
|
2022
|
+
}
|
|
2023
|
+
function resolveForPlan(configResult) {
|
|
2024
|
+
if (configResult.ok) {
|
|
2025
|
+
return resolveChecks(coreCheckRegistry(), configResult.config?.checks);
|
|
2026
|
+
}
|
|
2027
|
+
const core = resolveChecks(coreCheckRegistry(), undefined);
|
|
2028
|
+
return {
|
|
2029
|
+
resolved: core.resolved,
|
|
2030
|
+
failures: [{ key: configResult.path, reason: `invalid \`checks\` registration — ${configResult.error}` }]
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function printClassificationWarnings(configChecks) {
|
|
2034
|
+
const classification = resolveChecks(coreCheckRegistry(), configChecks);
|
|
2035
|
+
for (const entry2 of classification.resolved) {
|
|
2036
|
+
if (entry2.state === "overridden")
|
|
2037
|
+
process.stdout.write(`${overriddenNextMinorWarning(entry2.name)}
|
|
2038
|
+
`);
|
|
2039
|
+
}
|
|
2040
|
+
for (const failure of classification.failures) {
|
|
2041
|
+
process.stdout.write(`${bareKeyNextMinorWarning(failure.key)}
|
|
2042
|
+
`);
|
|
2043
|
+
}
|
|
1896
2044
|
}
|
|
1897
2045
|
async function checkCommand(args) {
|
|
1898
2046
|
const jsonOutput = args.includes("--json");
|
|
1899
2047
|
const diffOnly = args.includes("--diff-only");
|
|
1900
2048
|
const requestedParallel = parseParallel(args);
|
|
1901
2049
|
const allRequested = args.includes("--all");
|
|
2050
|
+
const planRequested = args.includes("--plan");
|
|
1902
2051
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
1903
2052
|
const requestedName = positional[0];
|
|
2053
|
+
if (planRequested) {
|
|
2054
|
+
const result = resolveForPlan(loadConfigChecked());
|
|
2055
|
+
if (jsonOutput) {
|
|
2056
|
+
process.stdout.write(`${JSON.stringify(renderPlanJson(result), null, 2)}
|
|
2057
|
+
`);
|
|
2058
|
+
} else {
|
|
2059
|
+
process.stdout.write(`${renderPlanTable(result)}
|
|
2060
|
+
`);
|
|
2061
|
+
}
|
|
2062
|
+
process.exit(result.failures.length > 0 ? 1 : 0);
|
|
2063
|
+
}
|
|
1904
2064
|
if (!allRequested && !requestedName) {
|
|
1905
|
-
console.error("Usage: vinaya check <name> | --all [--json] [--diff-only] [--parallel[=n]]");
|
|
2065
|
+
console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--parallel[=n]]");
|
|
1906
2066
|
process.exit(2);
|
|
1907
2067
|
}
|
|
1908
|
-
const { specs: customSpecs, errorOutcome } = customSpecsFromConfig();
|
|
2068
|
+
const { specs: customSpecs, errorOutcome, checks: configChecks } = customSpecsFromConfig();
|
|
1909
2069
|
const allSpecs = [...coreCheckRegistry(), ...customSpecs];
|
|
1910
2070
|
const specsToRun = allRequested ? allSpecs : allSpecs.filter((s) => s.name === requestedName);
|
|
1911
2071
|
if (!allRequested && specsToRun.length === 0) {
|
|
@@ -1935,6 +2095,7 @@ async function checkCommand(args) {
|
|
|
1935
2095
|
process.stdout.write(` ${e.severity}: ${e.message}
|
|
1936
2096
|
`);
|
|
1937
2097
|
}
|
|
2098
|
+
printClassificationWarnings(configChecks);
|
|
1938
2099
|
}
|
|
1939
2100
|
const failed = allOutcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
|
|
1940
2101
|
process.exit(failed ? 1 : 0);
|
|
@@ -2158,6 +2319,7 @@ var CONFIG_PATH = "vinaya.config.json";
|
|
|
2158
2319
|
var DOCTRINE_POINTER_PATH = "VINAYA.md";
|
|
2159
2320
|
var CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-checks.yml";
|
|
2160
2321
|
var REVIEW_WORKFLOW_PATH = ".github/workflows/vinaya-review.yml";
|
|
2322
|
+
var REVIEW_VERDICT_WORKFLOW_PATH = ".github/workflows/vinaya-review-verdict.yml";
|
|
2161
2323
|
var ARCHIVIST_WORKFLOW_PATH = ".github/workflows/vinaya-archivist.yml";
|
|
2162
2324
|
var MANAGED_NOTE = "Managed by Vinaya — created by `vinaya init`. `vinaya upgrade` regenerates it; `vinaya eject` removes it.";
|
|
2163
2325
|
function starterConfig() {
|
|
@@ -2197,6 +2359,8 @@ jobs:
|
|
|
2197
2359
|
runs-on: ubuntu-latest
|
|
2198
2360
|
permissions:
|
|
2199
2361
|
contents: read
|
|
2362
|
+
pull-requests: read
|
|
2363
|
+
issues: read
|
|
2200
2364
|
steps:
|
|
2201
2365
|
- uses: actions/checkout@v4
|
|
2202
2366
|
with:
|
|
@@ -2205,36 +2369,94 @@ jobs:
|
|
|
2205
2369
|
with:
|
|
2206
2370
|
node-version: 20
|
|
2207
2371
|
- name: Run checks
|
|
2372
|
+
env:
|
|
2373
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2374
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2375
|
+
BRANCH: \${{ github.head_ref }}
|
|
2208
2376
|
run: npx --yes @attalabs/vinaya check --all --diff-only
|
|
2209
2377
|
`;
|
|
2210
2378
|
}
|
|
2211
2379
|
function reviewWorkflow() {
|
|
2212
2380
|
return `# ${MANAGED_NOTE}
|
|
2213
2381
|
#
|
|
2214
|
-
# The review gate —
|
|
2215
|
-
#
|
|
2216
|
-
#
|
|
2217
|
-
# structurally cannot receive
|
|
2218
|
-
#
|
|
2382
|
+
# The required review gate — pull_request events only. The verdict-comment
|
|
2383
|
+
# half lives in its own workflow (vinaya-review-verdict.yml): a new PR
|
|
2384
|
+
# comment fires a different GitHub event that this pull_request-only
|
|
2385
|
+
# workflow structurally cannot receive — and keeping the comment path in a
|
|
2386
|
+
# separate FILE means this workflow's runs never list permanently-skipped
|
|
2387
|
+
# comment jobs on the PR's checks panel. When a clean final verdict lands,
|
|
2388
|
+
# the verdict workflow re-runs this one, so the required check below goes
|
|
2389
|
+
# green natively with no manual rerun.
|
|
2219
2390
|
name: Vinaya Review Gate
|
|
2220
2391
|
|
|
2221
2392
|
on:
|
|
2222
2393
|
pull_request:
|
|
2223
2394
|
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
2224
|
-
issue_comment:
|
|
2225
|
-
types: [created]
|
|
2226
2395
|
|
|
2227
2396
|
jobs:
|
|
2228
2397
|
vinaya-review:
|
|
2229
2398
|
name: vinaya review gate
|
|
2230
|
-
if: >
|
|
2231
|
-
github.event_name == 'pull_request' ||
|
|
2232
|
-
(github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT'))
|
|
2233
2399
|
runs-on: ubuntu-latest
|
|
2234
2400
|
permissions:
|
|
2235
2401
|
contents: read
|
|
2236
2402
|
pull-requests: read
|
|
2237
2403
|
issues: read
|
|
2404
|
+
steps:
|
|
2405
|
+
- uses: actions/checkout@v4
|
|
2406
|
+
with:
|
|
2407
|
+
# This job executes repo content; don't leave the token in
|
|
2408
|
+
# .git/config for scripts to read.
|
|
2409
|
+
persist-credentials: false
|
|
2410
|
+
fetch-depth: 0
|
|
2411
|
+
- uses: actions/setup-node@v4
|
|
2412
|
+
with:
|
|
2413
|
+
node-version: 20
|
|
2414
|
+
- name: Review gate
|
|
2415
|
+
env:
|
|
2416
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2417
|
+
# PR_NUMBER is what makes the review-gate check EVALUATE: without
|
|
2418
|
+
# it the adapter reads "no PR yet — local dev" and exits 0, and
|
|
2419
|
+
# the gate is green regardless of review state.
|
|
2420
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2421
|
+
BRANCH: \${{ github.head_ref }}
|
|
2422
|
+
run: npx --yes @attalabs/vinaya check --all
|
|
2423
|
+
`;
|
|
2424
|
+
}
|
|
2425
|
+
function reviewVerdictWorkflow() {
|
|
2426
|
+
return `# ${MANAGED_NOTE}
|
|
2427
|
+
#
|
|
2428
|
+
# The verdict-comment half of the review gate. A reviewer's verdict arrives
|
|
2429
|
+
# as a PR comment (\`VERDICT: APPROVE\` / \`VERDICT: PASS\`), which fires
|
|
2430
|
+
# GitHub's \`issue_comment\` event — an event the required pull_request
|
|
2431
|
+
# workflow cannot receive. This workflow evaluates the gate on that comment
|
|
2432
|
+
# and, when the evaluation is clean, RE-RUNS the required workflow so its
|
|
2433
|
+
# check goes green natively with no manual rerun. (Writing check-run
|
|
2434
|
+
# conclusions directly is no longer possible: GitHub's 2025-02-12 change
|
|
2435
|
+
# restricts check-run updates to the owning workflow — re-running is the
|
|
2436
|
+
# supported channel.)
|
|
2437
|
+
#
|
|
2438
|
+
# Privilege split, deliberate: \`evaluate\` checks out and executes repo
|
|
2439
|
+
# content and therefore holds NO write permission; \`retrigger\` holds
|
|
2440
|
+
# \`actions: write\` but checks out and executes nothing — its only inputs
|
|
2441
|
+
# are the evaluator's outputs, resolved via \`gh pr view\` before any repo
|
|
2442
|
+
# content ran. A malicious branch can at worst fail its own evaluation.
|
|
2443
|
+
name: Vinaya Review Gate (on verdict)
|
|
2444
|
+
|
|
2445
|
+
on:
|
|
2446
|
+
issue_comment:
|
|
2447
|
+
types: [created]
|
|
2448
|
+
|
|
2449
|
+
jobs:
|
|
2450
|
+
evaluate:
|
|
2451
|
+
name: vinaya review gate (verdict check)
|
|
2452
|
+
if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT')
|
|
2453
|
+
runs-on: ubuntu-latest
|
|
2454
|
+
permissions:
|
|
2455
|
+
contents: read
|
|
2456
|
+
pull-requests: read
|
|
2457
|
+
issues: read
|
|
2458
|
+
outputs:
|
|
2459
|
+
branch: \${{ steps.pr.outputs.branch }}
|
|
2238
2460
|
steps:
|
|
2239
2461
|
# issue_comment payloads carry no PR head SHA/branch — resolve them
|
|
2240
2462
|
# before checkout, and check out that exact commit (the event's default
|
|
@@ -2244,24 +2466,54 @@ jobs:
|
|
|
2244
2466
|
env:
|
|
2245
2467
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2246
2468
|
run: |
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
else
|
|
2250
|
-
NUMBER="\${{ github.event.issue.number }}"
|
|
2251
|
-
fi
|
|
2469
|
+
NUMBER="\${{ github.event.issue.number }}"
|
|
2470
|
+
BRANCH=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefName -q .headRefName)
|
|
2252
2471
|
SHA=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefOid -q .headRefOid)
|
|
2472
|
+
echo "number=$NUMBER" >> "$GITHUB_OUTPUT"
|
|
2473
|
+
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
2253
2474
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
2254
2475
|
- uses: actions/checkout@v4
|
|
2255
2476
|
with:
|
|
2256
2477
|
ref: \${{ steps.pr.outputs.sha }}
|
|
2478
|
+
persist-credentials: false
|
|
2257
2479
|
fetch-depth: 0
|
|
2258
2480
|
- uses: actions/setup-node@v4
|
|
2259
2481
|
with:
|
|
2260
2482
|
node-version: 20
|
|
2261
|
-
- name: Review gate
|
|
2483
|
+
- name: Review gate (verdict evaluation)
|
|
2262
2484
|
env:
|
|
2263
2485
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2264
|
-
|
|
2486
|
+
# Same wiring as the required workflow: PR_NUMBER is what makes
|
|
2487
|
+
# the adapter evaluate instead of no-op'ing as "local dev".
|
|
2488
|
+
PR_NUMBER: \${{ steps.pr.outputs.number }}
|
|
2489
|
+
BRANCH: \${{ steps.pr.outputs.branch }}
|
|
2490
|
+
run: npx --yes @attalabs/vinaya check review-gate
|
|
2491
|
+
|
|
2492
|
+
# Executes nothing; consumes only the evaluator's outputs. Fires only on a
|
|
2493
|
+
# clean evaluation — a failed one leaves the standing red untouched.
|
|
2494
|
+
retrigger:
|
|
2495
|
+
name: vinaya review gate (retrigger)
|
|
2496
|
+
if: needs.evaluate.result == 'success'
|
|
2497
|
+
needs: evaluate
|
|
2498
|
+
runs-on: ubuntu-latest
|
|
2499
|
+
permissions:
|
|
2500
|
+
actions: write
|
|
2501
|
+
steps:
|
|
2502
|
+
- name: Re-run the required review gate for this branch
|
|
2503
|
+
env:
|
|
2504
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2505
|
+
BRANCH: \${{ needs.evaluate.outputs.branch }}
|
|
2506
|
+
run: |
|
|
2507
|
+
RUN_ID=$(gh run list --repo "\${{ github.repository }}" \\
|
|
2508
|
+
--workflow vinaya-review.yml --branch "$BRANCH" \\
|
|
2509
|
+
--status completed \\
|
|
2510
|
+
--json databaseId,event \\
|
|
2511
|
+
--jq '[.[] | select(.event=="pull_request")][0].databaseId // empty')
|
|
2512
|
+
if [ -z "$RUN_ID" ]; then
|
|
2513
|
+
echo "No completed pull_request run of vinaya-review.yml for branch $BRANCH - nothing to re-run."
|
|
2514
|
+
exit 0
|
|
2515
|
+
fi
|
|
2516
|
+
gh run rerun "$RUN_ID" --repo "\${{ github.repository }}"
|
|
2265
2517
|
`;
|
|
2266
2518
|
}
|
|
2267
2519
|
function archivistWorkflow() {
|
|
@@ -2461,6 +2713,12 @@ function buildInitOps(ctx) {
|
|
|
2461
2713
|
const hookMode = 493;
|
|
2462
2714
|
ops.push({ kind: "create-file", path: CHECKS_WORKFLOW_PATH, content: checksWorkflow(), group: "CI workflows" });
|
|
2463
2715
|
ops.push({ kind: "create-file", path: REVIEW_WORKFLOW_PATH, content: reviewWorkflow(), group: "CI workflows" });
|
|
2716
|
+
ops.push({
|
|
2717
|
+
kind: "create-file",
|
|
2718
|
+
path: REVIEW_VERDICT_WORKFLOW_PATH,
|
|
2719
|
+
content: reviewVerdictWorkflow(),
|
|
2720
|
+
group: "CI workflows"
|
|
2721
|
+
});
|
|
2464
2722
|
ops.push({ kind: "create-file", path: ARCHIVIST_WORKFLOW_PATH, content: archivistWorkflow(), group: "CI workflows" });
|
|
2465
2723
|
ops.push({
|
|
2466
2724
|
kind: "managed-block",
|
|
@@ -3007,6 +3265,34 @@ function diagnoseEnvDeclarations(repoRoot, config) {
|
|
|
3007
3265
|
}
|
|
3008
3266
|
return findings;
|
|
3009
3267
|
}
|
|
3268
|
+
function diagnoseCheckClassification(config) {
|
|
3269
|
+
const classification = resolveChecks(coreCheckRegistry(), config?.checks);
|
|
3270
|
+
const findings = [];
|
|
3271
|
+
for (const entry2 of classification.resolved) {
|
|
3272
|
+
if (entry2.state === "overridden")
|
|
3273
|
+
findings.push(warn("checks", overriddenNextMinorWarning(entry2.name)));
|
|
3274
|
+
}
|
|
3275
|
+
for (const failure of classification.failures) {
|
|
3276
|
+
findings.push(warn("checks", bareKeyNextMinorWarning(failure.key)));
|
|
3277
|
+
}
|
|
3278
|
+
return findings;
|
|
3279
|
+
}
|
|
3280
|
+
function diagnoseGlobalConfigChecks() {
|
|
3281
|
+
if (!existsSync8(GLOBAL_CONFIG_PATH))
|
|
3282
|
+
return [];
|
|
3283
|
+
let raw;
|
|
3284
|
+
try {
|
|
3285
|
+
raw = JSON.parse(readFileSync5(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
3286
|
+
} catch (err) {
|
|
3287
|
+
return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
|
|
3288
|
+
}
|
|
3289
|
+
const parsed = VinayaConfigSchema.safeParse(raw);
|
|
3290
|
+
if (!parsed.success)
|
|
3291
|
+
return [];
|
|
3292
|
+
if (!parsed.data.checks || Object.keys(parsed.data.checks).length === 0)
|
|
3293
|
+
return [];
|
|
3294
|
+
return [warn("checks", globalChecksIgnoredWarning(GLOBAL_CONFIG_PATH))];
|
|
3295
|
+
}
|
|
3010
3296
|
async function diagnoseEnvironment(deps, hasDrift) {
|
|
3011
3297
|
const findings = [];
|
|
3012
3298
|
const auth = await deps.ghAuthStatus();
|
|
@@ -3077,6 +3363,8 @@ async function runDoctor(args, deps) {
|
|
|
3077
3363
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
3078
3364
|
}
|
|
3079
3365
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
3366
|
+
findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
|
|
3367
|
+
findings.push(...diagnoseGlobalConfigChecks());
|
|
3080
3368
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
3081
3369
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
3082
3370
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
@@ -4363,10 +4651,15 @@ var COMMANDS = [
|
|
|
4363
4651
|
{ flag: "--all", description: "Run every registered check instead of one named check" },
|
|
4364
4652
|
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" },
|
|
4365
4653
|
{ flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
|
|
4366
|
-
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
|
|
4654
|
+
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" },
|
|
4655
|
+
{
|
|
4656
|
+
flag: "--plan",
|
|
4657
|
+
description: "Print the resolved check registry (default/overridden/additive) without running anything"
|
|
4658
|
+
}
|
|
4367
4659
|
],
|
|
4368
4660
|
details: [
|
|
4369
|
-
"Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't."
|
|
4661
|
+
"Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't.",
|
|
4662
|
+
"`--plan` composes with `--json`. It requires zero env vars and never prints an env value — only how each one resolves (passthrough, optional, literal, or anyOf). A `FAIL_CLOSED` entry (a bare key with no namespace matching no core check) always renders inline rather than being dropped, and exits non-zero. `--plan` previews what the next minor's execution will enforce — `vinaya check` itself still runs the flat, unvalidated registry this release."
|
|
4370
4663
|
],
|
|
4371
4664
|
status: "shipped"
|
|
4372
4665
|
},
|
package/package.json
CHANGED