@attalabs/vinaya 0.9.0 → 0.16.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 +33 -7
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +7 -7
- package/aeg-root/glossary.md +4 -0
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/developer.md +29 -5
- package/aeg-root/roles/planner.md +3 -3
- package/aeg-root/roles/reviewer.md +17 -4
- package/aeg-root/roles/security.md +16 -5
- package/aeg-root/roles/tranche-archivist.md +1 -1
- package/aeg-root/skills/aeg/SKILL.md +1 -1
- package/aeg-root/state-machine.md +8 -8
- package/aeg-root/templates/pr-report-template.md +8 -4
- package/aeg-root/tranche-model.md +34 -12
- package/dist/checks/bin/check-branch-topology.js +232 -120
- package/dist/checks/bin/check-brief-shape.js +232 -120
- package/dist/checks/bin/check-closes-n.js +232 -120
- package/dist/checks/bin/check-coherence.js +234 -121
- package/dist/checks/bin/check-dead-branch-push.js +232 -120
- package/dist/checks/bin/check-dispatch-readiness.js +234 -121
- package/dist/checks/bin/check-doc-coverage-push.js +261 -130
- package/dist/checks/bin/check-doc-coverage.js +263 -132
- package/dist/checks/bin/check-evidence-fresh.js +3196 -0
- package/dist/checks/bin/check-first-push-dispatch.js +234 -121
- package/dist/checks/bin/check-issue-assignment.js +232 -120
- package/dist/checks/bin/check-no-disk-state.js +232 -120
- package/dist/checks/bin/check-reader-resolvable-prose.js +232 -120
- package/dist/checks/bin/check-registry-gates.js +232 -120
- package/dist/checks/bin/check-review-gate.js +237 -123
- package/dist/checks/bin/check-single-plan-pr.js +232 -120
- package/dist/checks/bin/check-test-plan.js +232 -120
- package/dist/index.js +1232 -262
- package/package.json +1 -1
- 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/required-server-files.json +3 -1
- 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/_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 +3 -3
- 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 +2 -3
- 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]__0e95dv5._.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]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__05t6482._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0mdkphe._.js +3 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0plfbyn._.js +45 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +51 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0zfjc2o._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1p19382._.js → [root-of-the-server]__1qpl4m_._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.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/{3antrj5z0uf_c.js → 0ek6qlqhokys0.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 0fm4p9os2lsom.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3zyedaek9nvr_.js → 0ywkq74falx-x.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1s9ixp5nhnevp.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
- package/templates/custom-check.template.ts +5 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1s7kj3s._.js +0 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1tswrm4._.js +0 -9
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01jl99k._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_20c6-3m._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2e68sbtdhi5dg.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as dirname8, join as
|
|
4
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
5
|
+
import { dirname as dirname8, join as join22 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/commands/archive.ts
|
|
9
|
-
import { execFileSync as
|
|
10
|
-
// ../../packages/aeg-
|
|
9
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
10
|
+
// ../../packages/aeg-forge-state/src/strip-code.ts
|
|
11
11
|
function maskCode(body) {
|
|
12
12
|
const fill = (line) => " ".repeat(line.length);
|
|
13
13
|
return maskIndentedCode(maskFencedCode(body, fill), fill).replace(/(`+)[^\n]*?\1/g, (m) => " ".repeat(m.length));
|
|
@@ -24,29 +24,34 @@ function stripCode(body, options = {}) {
|
|
|
24
24
|
var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
|
|
25
25
|
var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
|
|
26
26
|
function maskFencedCode(body, fill) {
|
|
27
|
+
const { lines } = scanFencedCode(body);
|
|
28
|
+
return lines.map(({ line, inCode }) => inCode ? fill(line) : line).join(`
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
function scanFencedCode(body) {
|
|
27
32
|
let fenceChar = null;
|
|
28
33
|
let fenceLen = 0;
|
|
29
|
-
|
|
34
|
+
const lines = body.split(`
|
|
30
35
|
`).map((line) => {
|
|
31
36
|
if (fenceChar === null) {
|
|
32
37
|
const open = line.match(FENCE_OPEN);
|
|
33
38
|
if (!open)
|
|
34
|
-
return line;
|
|
39
|
+
return { line, inCode: false };
|
|
35
40
|
const marker = open[1];
|
|
36
41
|
if (marker[0] === "`" && open[2].includes("`"))
|
|
37
|
-
return line;
|
|
42
|
+
return { line, inCode: false };
|
|
38
43
|
fenceChar = marker[0];
|
|
39
44
|
fenceLen = marker.length;
|
|
40
|
-
return
|
|
45
|
+
return { line, inCode: true };
|
|
41
46
|
}
|
|
42
47
|
const close = line.match(FENCE_CLOSE);
|
|
43
48
|
if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
|
|
44
49
|
fenceChar = null;
|
|
45
50
|
fenceLen = 0;
|
|
46
51
|
}
|
|
47
|
-
return
|
|
48
|
-
})
|
|
49
|
-
|
|
52
|
+
return { line, inCode: true };
|
|
53
|
+
});
|
|
54
|
+
return { lines, unterminated: fenceChar !== null };
|
|
50
55
|
}
|
|
51
56
|
var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
|
|
52
57
|
function indentWidth(line) {
|
|
@@ -89,88 +94,7 @@ function maskIndentedCode(body, fill) {
|
|
|
89
94
|
return out.join(`
|
|
90
95
|
`);
|
|
91
96
|
}
|
|
92
|
-
|
|
93
|
-
const masked = maskCode(body);
|
|
94
|
-
const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
|
|
95
|
-
if (!start)
|
|
96
|
-
return null;
|
|
97
|
-
const afterStart = start.index + start[0].length;
|
|
98
|
-
const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(afterStart));
|
|
99
|
-
if (!end)
|
|
100
|
-
return null;
|
|
101
|
-
return body.slice(afterStart, afterStart + end.index);
|
|
102
|
-
}
|
|
103
|
-
// ../../packages/aeg-core/src/parse-registry.ts
|
|
104
|
-
function parseRegistry(md) {
|
|
105
|
-
const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
|
|
106
|
-
const projects = [];
|
|
107
|
-
for (const row of rows) {
|
|
108
|
-
if (row.length < 4)
|
|
109
|
-
continue;
|
|
110
|
-
const name = stripBackticks(row[0] ?? "").trim();
|
|
111
|
-
const path = stripBackticks(row[1] ?? "").trim();
|
|
112
|
-
const specsPath = stripBackticks(row[2] ?? "").trim();
|
|
113
|
-
const stateCell = (row[3] ?? "").trim();
|
|
114
|
-
if (!name || !path || !specsPath)
|
|
115
|
-
continue;
|
|
116
|
-
projects.push({
|
|
117
|
-
name,
|
|
118
|
-
path,
|
|
119
|
-
specsPath,
|
|
120
|
-
statePath: parseStateCell(stateCell)
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return projects;
|
|
124
|
-
}
|
|
125
|
-
function parseStateCell(cell) {
|
|
126
|
-
if (!cell)
|
|
127
|
-
return null;
|
|
128
|
-
const stripped = stripBackticks(cell).trim();
|
|
129
|
-
if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
|
|
130
|
-
return stripped;
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
function stripBackticks(s) {
|
|
135
|
-
return s.replace(/`/g, "");
|
|
136
|
-
}
|
|
137
|
-
function readMarkdownTable(md, headingMatcher) {
|
|
138
|
-
const lines = md.split(/\r?\n/);
|
|
139
|
-
let i = 0;
|
|
140
|
-
for (;i < lines.length; i++) {
|
|
141
|
-
if (headingMatcher.test(lines[i] ?? "")) {
|
|
142
|
-
i++;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (i >= lines.length)
|
|
147
|
-
return [];
|
|
148
|
-
for (;i < lines.length; i++) {
|
|
149
|
-
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
if (i + 1 >= lines.length)
|
|
153
|
-
return [];
|
|
154
|
-
i += 2;
|
|
155
|
-
const out = [];
|
|
156
|
-
for (;i < lines.length; i++) {
|
|
157
|
-
const trimmed = (lines[i] ?? "").trim();
|
|
158
|
-
if (!trimmed.startsWith("|"))
|
|
159
|
-
break;
|
|
160
|
-
out.push(splitTableRow(trimmed));
|
|
161
|
-
}
|
|
162
|
-
return out;
|
|
163
|
-
}
|
|
164
|
-
function splitTableRow(row) {
|
|
165
|
-
const PLACEHOLDER = "\x00PIPE\x00";
|
|
166
|
-
const escaped = row.replace(/\\\|/g, PLACEHOLDER);
|
|
167
|
-
const parts = escaped.split("|").map((s) => s.trim());
|
|
168
|
-
if (parts.length && parts[0] === "")
|
|
169
|
-
parts.shift();
|
|
170
|
-
if (parts.length && parts[parts.length - 1] === "")
|
|
171
|
-
parts.pop();
|
|
172
|
-
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
173
|
-
}
|
|
97
|
+
|
|
174
98
|
// ../../packages/aeg-forge-state/src/parse-rationale-deps.ts
|
|
175
99
|
var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
|
|
176
100
|
// ../../packages/aeg-forge-state/src/gh.ts
|
|
@@ -378,6 +302,175 @@ function parseOwnerRepo(value) {
|
|
|
378
302
|
return null;
|
|
379
303
|
return { owner: match[1], repo: match[2] };
|
|
380
304
|
}
|
|
305
|
+
// ../../packages/aeg-core/src/anchored-region.ts
|
|
306
|
+
function anchoredRegionBounds(body, field) {
|
|
307
|
+
const masked = maskCode(body);
|
|
308
|
+
const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
|
|
309
|
+
if (!start)
|
|
310
|
+
return null;
|
|
311
|
+
const innerStart = start.index + start[0].length;
|
|
312
|
+
const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(innerStart));
|
|
313
|
+
if (!end)
|
|
314
|
+
return null;
|
|
315
|
+
const innerEnd = innerStart + end.index;
|
|
316
|
+
return { outerStart: start.index, outerEnd: innerEnd + end[0].length, innerStart, innerEnd };
|
|
317
|
+
}
|
|
318
|
+
function anchoredRegion(body, field) {
|
|
319
|
+
const bounds = anchoredRegionBounds(body, field);
|
|
320
|
+
if (!bounds)
|
|
321
|
+
return null;
|
|
322
|
+
return body.slice(bounds.innerStart, bounds.innerEnd);
|
|
323
|
+
}
|
|
324
|
+
// ../../packages/aeg-core/src/parse-registry.ts
|
|
325
|
+
function parseRegistry(md) {
|
|
326
|
+
const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
|
|
327
|
+
const projects = [];
|
|
328
|
+
for (const row of rows) {
|
|
329
|
+
if (row.length < 4)
|
|
330
|
+
continue;
|
|
331
|
+
const name = stripBackticks(row[0] ?? "").trim();
|
|
332
|
+
const path = stripBackticks(row[1] ?? "").trim();
|
|
333
|
+
const specsPath = stripBackticks(row[2] ?? "").trim();
|
|
334
|
+
const stateCell = (row[3] ?? "").trim();
|
|
335
|
+
if (!name || !path || !specsPath)
|
|
336
|
+
continue;
|
|
337
|
+
projects.push({
|
|
338
|
+
name,
|
|
339
|
+
path,
|
|
340
|
+
specsPath,
|
|
341
|
+
statePath: parseStateCell(stateCell)
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return projects;
|
|
345
|
+
}
|
|
346
|
+
function parseStateCell(cell) {
|
|
347
|
+
if (!cell)
|
|
348
|
+
return null;
|
|
349
|
+
const stripped = stripBackticks(cell).trim();
|
|
350
|
+
if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
|
|
351
|
+
return stripped;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
function stripBackticks(s) {
|
|
356
|
+
return s.replace(/`/g, "");
|
|
357
|
+
}
|
|
358
|
+
function readMarkdownTable(md, headingMatcher) {
|
|
359
|
+
const lines = md.split(/\r?\n/);
|
|
360
|
+
let i = 0;
|
|
361
|
+
for (;i < lines.length; i++) {
|
|
362
|
+
if (headingMatcher.test(lines[i] ?? "")) {
|
|
363
|
+
i++;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (i >= lines.length)
|
|
368
|
+
return [];
|
|
369
|
+
for (;i < lines.length; i++) {
|
|
370
|
+
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
if (i + 1 >= lines.length)
|
|
374
|
+
return [];
|
|
375
|
+
i += 2;
|
|
376
|
+
const out = [];
|
|
377
|
+
for (;i < lines.length; i++) {
|
|
378
|
+
const trimmed = (lines[i] ?? "").trim();
|
|
379
|
+
if (!trimmed.startsWith("|"))
|
|
380
|
+
break;
|
|
381
|
+
out.push(splitTableRow(trimmed));
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
function splitTableRow(row) {
|
|
386
|
+
const PLACEHOLDER = "\x00PIPE\x00";
|
|
387
|
+
const escaped = row.replace(/\\\|/g, PLACEHOLDER);
|
|
388
|
+
const parts = escaped.split("|").map((s) => s.trim());
|
|
389
|
+
if (parts.length && parts[0] === "")
|
|
390
|
+
parts.shift();
|
|
391
|
+
if (parts.length && parts[parts.length - 1] === "")
|
|
392
|
+
parts.pop();
|
|
393
|
+
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
394
|
+
}
|
|
395
|
+
// ../../packages/aeg-core/src/blast-radius-domains.ts
|
|
396
|
+
function resolveWorkspaceEntry(entry2, listDirs) {
|
|
397
|
+
if (entry2.endsWith("/*")) {
|
|
398
|
+
const baseDir = entry2.slice(0, -2);
|
|
399
|
+
return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
|
|
400
|
+
}
|
|
401
|
+
if (entry2.includes("*"))
|
|
402
|
+
return [];
|
|
403
|
+
return [entry2];
|
|
404
|
+
}
|
|
405
|
+
function deriveWorkspacePackageDomains(workspaces, listDirs) {
|
|
406
|
+
const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
|
|
407
|
+
const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
|
|
408
|
+
const excluded = new Set;
|
|
409
|
+
for (const entry2 of negative) {
|
|
410
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs))
|
|
411
|
+
excluded.add(member);
|
|
412
|
+
}
|
|
413
|
+
const domains = new Set;
|
|
414
|
+
for (const entry2 of positive) {
|
|
415
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
|
|
416
|
+
if (excluded.has(member))
|
|
417
|
+
continue;
|
|
418
|
+
const segments = member.split("/");
|
|
419
|
+
if (segments[0] !== "packages" || segments.length < 2)
|
|
420
|
+
continue;
|
|
421
|
+
domains.add(`packages/${segments[1]}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return [...domains].sort();
|
|
425
|
+
}
|
|
426
|
+
function parsePnpmWorkspaceYaml(content) {
|
|
427
|
+
const unquote = (s) => {
|
|
428
|
+
const t = s.trim();
|
|
429
|
+
if (t.length >= 2 && (t[0] === "'" && t.at(-1) === "'" || t[0] === '"' && t.at(-1) === '"')) {
|
|
430
|
+
return t.slice(1, -1);
|
|
431
|
+
}
|
|
432
|
+
return t;
|
|
433
|
+
};
|
|
434
|
+
const inlineMatch = content.match(/^packages:\s*\[(.*)\]\s*$/m);
|
|
435
|
+
if (inlineMatch) {
|
|
436
|
+
return (inlineMatch[1] ?? "").split(",").map(unquote).filter((s) => s.length > 0);
|
|
437
|
+
}
|
|
438
|
+
const entries = [];
|
|
439
|
+
let inBlock = false;
|
|
440
|
+
for (const rawLine of content.split(`
|
|
441
|
+
`)) {
|
|
442
|
+
const line = rawLine.replace(/#.*$/, "");
|
|
443
|
+
if (/^packages:\s*$/.test(line)) {
|
|
444
|
+
inBlock = true;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (!inBlock)
|
|
448
|
+
continue;
|
|
449
|
+
const itemMatch = line.match(/^\s+-\s*(.+?)\s*$/);
|
|
450
|
+
if (itemMatch?.[1]) {
|
|
451
|
+
entries.push(unquote(itemMatch[1]));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (line.trim().length === 0)
|
|
455
|
+
continue;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
return entries;
|
|
459
|
+
}
|
|
460
|
+
var CROSS_CUTTING_CANDIDATES = [
|
|
461
|
+
"bun.lock",
|
|
462
|
+
"package-lock.json",
|
|
463
|
+
"pnpm-lock.yaml",
|
|
464
|
+
"yarn.lock",
|
|
465
|
+
"turbo.json",
|
|
466
|
+
"biome.json",
|
|
467
|
+
"tsconfig.json",
|
|
468
|
+
".github/workflows",
|
|
469
|
+
".husky"
|
|
470
|
+
];
|
|
471
|
+
function deriveBuiltinCrossCuttingDefaults(exists) {
|
|
472
|
+
return CROSS_CUTTING_CANDIDATES.filter((path) => exists(path));
|
|
473
|
+
}
|
|
381
474
|
// ../../packages/aeg-core/src/state-machine-model.ts
|
|
382
475
|
var FORGE_FACT_INPUTS = [
|
|
383
476
|
{
|
|
@@ -443,12 +536,30 @@ var DERIVED_STATUSES = [
|
|
|
443
536
|
"incoherent"
|
|
444
537
|
];
|
|
445
538
|
var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
|
|
539
|
+
// ../../packages/aeg-core/src/file-classify.ts
|
|
540
|
+
function isCodeFile(p) {
|
|
541
|
+
if (p.endsWith(".md"))
|
|
542
|
+
return false;
|
|
543
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
|
|
544
|
+
}
|
|
446
545
|
// ../../packages/aeg-core/src/waiver-label.ts
|
|
447
546
|
var WAIVER_LABEL = label("waiver-docs");
|
|
448
547
|
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
548
|
+
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
549
|
+
function isPrincipal(login, principalAllowlist) {
|
|
550
|
+
if (login === null)
|
|
551
|
+
return false;
|
|
552
|
+
const normalized = login.toLowerCase();
|
|
553
|
+
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
554
|
+
}
|
|
449
555
|
|
|
450
556
|
// ../../packages/aeg-core/src/doc-owners.ts
|
|
451
557
|
var DOC_OWNERS_PATH = ".vinaya/doc-owners";
|
|
558
|
+
function classifyDocOwnersManifest(content) {
|
|
559
|
+
if (content === null)
|
|
560
|
+
return "absent";
|
|
561
|
+
return content.trim() === "" ? "empty" : "present";
|
|
562
|
+
}
|
|
452
563
|
function parseDocOwners(content) {
|
|
453
564
|
const bindings = [];
|
|
454
565
|
const errors = [];
|
|
@@ -493,6 +604,13 @@ function globToRegex(pat) {
|
|
|
493
604
|
re += "$";
|
|
494
605
|
return new RegExp(re);
|
|
495
606
|
}
|
|
607
|
+
function isUrlPointer(p) {
|
|
608
|
+
return /^https?:\/\//.test(p);
|
|
609
|
+
}
|
|
610
|
+
function pointerToPath(p) {
|
|
611
|
+
const idx = p.indexOf("#");
|
|
612
|
+
return idx === -1 ? p : p.slice(0, idx);
|
|
613
|
+
}
|
|
496
614
|
var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
|
|
497
615
|
// ../../packages/aeg-core/src/pr-tier.ts
|
|
498
616
|
function readTierFromPrBody(prBody) {
|
|
@@ -779,15 +897,25 @@ function isTaskIssueLabelSet(labels) {
|
|
|
779
897
|
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
780
898
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
781
899
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
900
|
+
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
901
|
+
function extractHeadSha(comment) {
|
|
902
|
+
const m = comment.match(HEAD_SHA_PATTERN);
|
|
903
|
+
return m ? m[1].toLowerCase() : null;
|
|
904
|
+
}
|
|
782
905
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
783
906
|
const clearHits = comments.filter((c) => valuePattern.test(c));
|
|
784
907
|
if (clearHits.length > 0) {
|
|
785
908
|
const latest = clearHits[clearHits.length - 1];
|
|
786
909
|
const m = latest.match(valuePattern);
|
|
787
|
-
return {
|
|
910
|
+
return {
|
|
911
|
+
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
912
|
+
headSha: extractHeadSha(latest),
|
|
913
|
+
danglingNote: null
|
|
914
|
+
};
|
|
788
915
|
}
|
|
789
916
|
return {
|
|
790
917
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
918
|
+
headSha: null,
|
|
791
919
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
792
920
|
};
|
|
793
921
|
}
|
|
@@ -970,6 +1098,10 @@ function starterConfig() {
|
|
|
970
1098
|
}
|
|
971
1099
|
};
|
|
972
1100
|
}
|
|
1101
|
+
function ownVersion() {
|
|
1102
|
+
const pkg = JSON.parse(readFileSync(join2(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
1103
|
+
return pkg.version;
|
|
1104
|
+
}
|
|
973
1105
|
var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
|
|
974
1106
|
function vinayaSetupSteps(selfHost) {
|
|
975
1107
|
if (!selfHost)
|
|
@@ -1001,7 +1133,7 @@ function vinayaSetupSteps(selfHost) {
|
|
|
1001
1133
|
`;
|
|
1002
1134
|
}
|
|
1003
1135
|
function vinayaRun(selfHost, args) {
|
|
1004
|
-
return selfHost ? `node ${selfHost.bin} ${args}` : `npx --yes @attalabs/vinaya ${args}`;
|
|
1136
|
+
return selfHost ? `node ${selfHost.bin} ${args}` : `npx --yes @attalabs/vinaya@${ownVersion()} ${args}`;
|
|
1005
1137
|
}
|
|
1006
1138
|
function adopterSetupStep(ciSetup) {
|
|
1007
1139
|
if (!ciSetup)
|
|
@@ -1427,10 +1559,6 @@ ${vinayaSetupSteps(selfHost)} - name: Run vinaya audit --only=direct-push
|
|
|
1427
1559
|
}
|
|
1428
1560
|
var HOOK_PREAMBLE = `#!/usr/bin/env sh
|
|
1429
1561
|
`;
|
|
1430
|
-
function ownVersion() {
|
|
1431
|
-
const pkg = JSON.parse(readFileSync(join2(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
1432
|
-
return pkg.version;
|
|
1433
|
-
}
|
|
1434
1562
|
function hookRun(selfHost, args) {
|
|
1435
1563
|
if (!selfHost)
|
|
1436
1564
|
return `npx --yes @attalabs/vinaya@${ownVersion()} ${args} || exit 1`;
|
|
@@ -1522,6 +1650,14 @@ function starterDocOwners() {
|
|
|
1522
1650
|
# This file absent, OR no glob matches any changed code file → silent no-op.
|
|
1523
1651
|
# The gate has no opinion until you teach it one.
|
|
1524
1652
|
#
|
|
1653
|
+
# \`vinaya doctor\` (never \`vinaya check\`) separately reports — it does not
|
|
1654
|
+
# fail the check — any binding whose code glob matches zero tracked files
|
|
1655
|
+
# anywhere in the repo, or whose in-repo pointer doesn't exist on disk. This
|
|
1656
|
+
# catches what the dormancy rule above structurally cannot: a glob matching
|
|
1657
|
+
# nothing trivially satisfies "no changed file matched" on every diff,
|
|
1658
|
+
# forever, so a binding naming deleted or renamed code reads as healthy
|
|
1659
|
+
# indefinitely unless something walks the whole repo, not just one diff.
|
|
1660
|
+
#
|
|
1525
1661
|
# ── No-doc allow-list ───────────────────────────────────────────────────────
|
|
1526
1662
|
# Surfaces that legitimately need no bound doc are listed below. Format:
|
|
1527
1663
|
# # no-doc: <glob> — <reason>
|
|
@@ -1654,11 +1790,12 @@ function buildInitProductOps(name) {
|
|
|
1654
1790
|
}
|
|
1655
1791
|
|
|
1656
1792
|
// src/lib/ops.ts
|
|
1657
|
-
import { execFileSync as
|
|
1793
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1658
1794
|
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1659
1795
|
import { dirname as dirname3, isAbsolute, join as join4, resolve, sep } from "node:path";
|
|
1660
1796
|
|
|
1661
1797
|
// src/lib/config.ts
|
|
1798
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1662
1799
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
1663
1800
|
import { homedir } from "node:os";
|
|
1664
1801
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
@@ -1786,7 +1923,8 @@ var VinayaConfigSchema = z.object({
|
|
|
1786
1923
|
briefSchema: BriefSchemaSchema.optional(),
|
|
1787
1924
|
managed: ManagedManifestSchema.optional(),
|
|
1788
1925
|
principals: z.array(z.string()).min(1).optional(),
|
|
1789
|
-
ci: z.object({ setup: z.string().min(1) }).optional()
|
|
1926
|
+
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
1927
|
+
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
|
|
1790
1928
|
});
|
|
1791
1929
|
var GLOBAL_VINAYA_HOME = join3(homedir(), ".vinaya");
|
|
1792
1930
|
var GLOBAL_CONFIG_PATH = join3(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -1833,6 +1971,75 @@ function stripGlobalOnlyKeys(config, path) {
|
|
|
1833
1971
|
}
|
|
1834
1972
|
return result;
|
|
1835
1973
|
}
|
|
1974
|
+
function resolvePrincipalAllowlist(config) {
|
|
1975
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
1976
|
+
}
|
|
1977
|
+
function trustAnchorRepo() {
|
|
1978
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
1979
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
1980
|
+
if (fromRunner) {
|
|
1981
|
+
const validated = wellFormed(fromRunner);
|
|
1982
|
+
if (validated)
|
|
1983
|
+
return validated;
|
|
1984
|
+
}
|
|
1985
|
+
try {
|
|
1986
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
1987
|
+
encoding: "utf-8",
|
|
1988
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1989
|
+
}).trim();
|
|
1990
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
1991
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
1992
|
+
} catch {
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function ghFetchTrustAnchorConfig() {
|
|
1997
|
+
const repo = trustAnchorRepo();
|
|
1998
|
+
if (!repo)
|
|
1999
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
2000
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
2001
|
+
encoding: "utf-8",
|
|
2002
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
function firstLine(err) {
|
|
2006
|
+
return err?.message?.split(`
|
|
2007
|
+
`)[0] ?? "unknown error";
|
|
2008
|
+
}
|
|
2009
|
+
function isMissingFileError(err) {
|
|
2010
|
+
const stderr = err?.stderr;
|
|
2011
|
+
const haystack = [
|
|
2012
|
+
err?.message ?? "",
|
|
2013
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
2014
|
+
].join(`
|
|
2015
|
+
`);
|
|
2016
|
+
return /\b404\b|not found/i.test(haystack);
|
|
2017
|
+
}
|
|
2018
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
2019
|
+
const warn = (why) => process.stdout.write(`⚠ could not read \`principals\` from the default branch (${why}) — falling back to vinaya's built-in principal allowlist.
|
|
2020
|
+
`);
|
|
2021
|
+
let base64;
|
|
2022
|
+
try {
|
|
2023
|
+
base64 = fetcher().trim();
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
if (!isMissingFileError(err))
|
|
2026
|
+
warn(firstLine(err));
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
if (!base64)
|
|
2030
|
+
return null;
|
|
2031
|
+
try {
|
|
2032
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
2033
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
2034
|
+
if (parsed.success)
|
|
2035
|
+
return parsed.data;
|
|
2036
|
+
warn("it did not satisfy the config schema");
|
|
2037
|
+
return null;
|
|
2038
|
+
} catch {
|
|
2039
|
+
warn("it was not readable as JSON");
|
|
2040
|
+
return null;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1836
2043
|
function readRepoCiSetup(repoRoot) {
|
|
1837
2044
|
const p = join3(repoRoot, "vinaya.config.json");
|
|
1838
2045
|
if (!existsSync2(p))
|
|
@@ -1883,7 +2090,7 @@ function resolveManagedBlockPath(repoRoot, opPath) {
|
|
|
1883
2090
|
if (!opPath.startsWith(".git/"))
|
|
1884
2091
|
return join4(repoRoot, opPath);
|
|
1885
2092
|
try {
|
|
1886
|
-
const commonDir =
|
|
2093
|
+
const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
|
|
1887
2094
|
encoding: "utf8"
|
|
1888
2095
|
}).trim();
|
|
1889
2096
|
const gitDir = isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
|
|
@@ -2449,7 +2656,7 @@ function realDeps() {
|
|
|
2449
2656
|
return { detectRepo: detectGitRepo };
|
|
2450
2657
|
}
|
|
2451
2658
|
function sh(args, input) {
|
|
2452
|
-
return
|
|
2659
|
+
return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
2453
2660
|
}
|
|
2454
2661
|
function shJson(args) {
|
|
2455
2662
|
return JSON.parse(sh(args));
|
|
@@ -2625,7 +2832,7 @@ async function archiveTrancheCommand(args) {
|
|
|
2625
2832
|
}
|
|
2626
2833
|
|
|
2627
2834
|
// src/commands/audit.ts
|
|
2628
|
-
import { execFileSync as
|
|
2835
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2629
2836
|
|
|
2630
2837
|
// src/lib/envelope.ts
|
|
2631
2838
|
var ENVELOPE_SCHEMA_VERSION = 1;
|
|
@@ -2653,7 +2860,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
|
|
|
2653
2860
|
var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
|
|
2654
2861
|
function shSoft(args, input) {
|
|
2655
2862
|
try {
|
|
2656
|
-
return
|
|
2863
|
+
return execFileSync5(args[0], args.slice(1), {
|
|
2657
2864
|
encoding: "utf8",
|
|
2658
2865
|
input,
|
|
2659
2866
|
timeout: COMMAND_TIMEOUT_MS
|
|
@@ -2677,7 +2884,7 @@ function shSoftJson(args) {
|
|
|
2677
2884
|
}
|
|
2678
2885
|
}
|
|
2679
2886
|
function sh2(args, input) {
|
|
2680
|
-
return
|
|
2887
|
+
return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
2681
2888
|
}
|
|
2682
2889
|
function listTaskBranches() {
|
|
2683
2890
|
const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
|
|
@@ -2927,7 +3134,7 @@ async function auditCommand(args) {
|
|
|
2927
3134
|
}
|
|
2928
3135
|
|
|
2929
3136
|
// src/commands/check.ts
|
|
2930
|
-
import { execFileSync as
|
|
3137
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2931
3138
|
|
|
2932
3139
|
// src/checks/contract.ts
|
|
2933
3140
|
var CHECK_SCHEMA_VERSION = 1;
|
|
@@ -3132,17 +3339,31 @@ function coreCheckRegistry() {
|
|
|
3132
3339
|
GITHUB_TOKEN: { optional: true },
|
|
3133
3340
|
GH_TOKEN: { optional: true }
|
|
3134
3341
|
}
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3342
|
+
},
|
|
3343
|
+
{
|
|
3344
|
+
name: "evidence-fresh",
|
|
3345
|
+
run: bin("check-evidence-fresh"),
|
|
3346
|
+
scope: "diff",
|
|
3347
|
+
timeoutMs: 15000,
|
|
3348
|
+
requiresOpenPr: true,
|
|
3349
|
+
env: {
|
|
3350
|
+
PR_NUMBER: { optional: true },
|
|
3351
|
+
PR_BODY: { optional: true },
|
|
3352
|
+
BASE_SHA: { optional: true },
|
|
3353
|
+
GITHUB_TOKEN: { optional: true },
|
|
3354
|
+
GH_TOKEN: { optional: true }
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
];
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// src/checks/resolver.ts
|
|
3361
|
+
var NAMESPACE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
|
|
3362
|
+
var RESERVED_PREFIX = "vinaya";
|
|
3363
|
+
function isValidNamespacedKey(key) {
|
|
3364
|
+
const parts = key.split("/");
|
|
3365
|
+
if (parts.length !== 2)
|
|
3366
|
+
return false;
|
|
3146
3367
|
const [prefix, rest] = parts;
|
|
3147
3368
|
if (!prefix || !rest)
|
|
3148
3369
|
return false;
|
|
@@ -3177,11 +3398,16 @@ function resolveChecks(core, configChecks) {
|
|
|
3177
3398
|
}
|
|
3178
3399
|
return { resolved, failures };
|
|
3179
3400
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3401
|
+
var MAX_KEY_DISPLAY_LENGTH = 80;
|
|
3402
|
+
function sanitizeKeyForDisplay(key) {
|
|
3403
|
+
const stripped = key.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
|
|
3404
|
+
return stripped.length > MAX_KEY_DISPLAY_LENGTH ? `${stripped.slice(0, MAX_KEY_DISPLAY_LENGTH)}…` : stripped;
|
|
3405
|
+
}
|
|
3406
|
+
function overriddenReplacesCoreDiagnostic(name) {
|
|
3407
|
+
return `check "${sanitizeKeyForDisplay(name)}" shares its name with a core check — it REPLACES that core check. The core check does not run.`;
|
|
3182
3408
|
}
|
|
3183
|
-
function
|
|
3184
|
-
return `check "${key}" has no namespace and matches no core check — it
|
|
3409
|
+
function bareKeyRejectedDiagnostic(key) {
|
|
3410
|
+
return `check "${sanitizeKeyForDisplay(key)}" has no namespace and matches no core check — it is REJECTED, and \`vinaya check\` refuses the whole run rather than executing a partial ruleset. Rename it to "<yourname>/<id>", both segments matching [a-z0-9][a-z0-9-]* — prefixing alone is not enough when the bare name itself breaks that grammar ("my_check", "QALint" need a real rename, not just a prefix).`;
|
|
3185
3411
|
}
|
|
3186
3412
|
|
|
3187
3413
|
// src/checks/runner.ts
|
|
@@ -3445,7 +3671,7 @@ async function runChecks(specs, opts) {
|
|
|
3445
3671
|
// src/commands/check.ts
|
|
3446
3672
|
function git(args) {
|
|
3447
3673
|
try {
|
|
3448
|
-
return
|
|
3674
|
+
return execFileSync6("git", args, { encoding: "utf8" }).trim();
|
|
3449
3675
|
} catch {
|
|
3450
3676
|
return "";
|
|
3451
3677
|
}
|
|
@@ -3466,26 +3692,6 @@ function parseParallel(args) {
|
|
|
3466
3692
|
}
|
|
3467
3693
|
return;
|
|
3468
3694
|
}
|
|
3469
|
-
function configErrorOutcome(path, error) {
|
|
3470
|
-
const finding = {
|
|
3471
|
-
schema: 1,
|
|
3472
|
-
check: "config",
|
|
3473
|
-
severity: "error",
|
|
3474
|
-
message: `${path}: invalid \`checks\` registration — ${error}`,
|
|
3475
|
-
agent_recovery_prompt: `Fix the invalid key/value named above in ${path}, then re-run \`vinaya check\`.`
|
|
3476
|
-
};
|
|
3477
|
-
return { name: "config", status: "error", exitCode: null, errors: [finding], durationMs: 0 };
|
|
3478
|
-
}
|
|
3479
|
-
function customSpecsFromConfig() {
|
|
3480
|
-
const result = loadConfigChecked();
|
|
3481
|
-
if (!result.ok)
|
|
3482
|
-
return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error), checks: undefined };
|
|
3483
|
-
const checks = result.config?.checks;
|
|
3484
|
-
if (!checks)
|
|
3485
|
-
return { specs: [], errorOutcome: null, checks: undefined };
|
|
3486
|
-
const specs = Object.entries(checks).map(([name, entry2]) => ({ name, ...entry2 }));
|
|
3487
|
-
return { specs, errorOutcome: null, checks };
|
|
3488
|
-
}
|
|
3489
3695
|
function classifyEnvValue(value) {
|
|
3490
3696
|
if (value === true)
|
|
3491
3697
|
return "passthrough";
|
|
@@ -3539,27 +3745,46 @@ function renderPlanTable(result) {
|
|
|
3539
3745
|
return table.map((row) => row.map((cell, col) => (cell ?? "").padEnd(widths[col] ?? 0)).join(" ")).join(`
|
|
3540
3746
|
`);
|
|
3541
3747
|
}
|
|
3542
|
-
function
|
|
3543
|
-
|
|
3544
|
-
|
|
3748
|
+
function duplicateIdFailures(resolved) {
|
|
3749
|
+
const seen = new Set;
|
|
3750
|
+
const reported = new Set;
|
|
3751
|
+
const failures = [];
|
|
3752
|
+
for (const entry2 of resolved) {
|
|
3753
|
+
if (!seen.has(entry2.name)) {
|
|
3754
|
+
seen.add(entry2.name);
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
if (reported.has(entry2.name))
|
|
3758
|
+
continue;
|
|
3759
|
+
reported.add(entry2.name);
|
|
3760
|
+
failures.push({ key: entry2.name, reason: "duplicate check id — two resolved entries claim the same name" });
|
|
3545
3761
|
}
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3762
|
+
return failures;
|
|
3763
|
+
}
|
|
3764
|
+
var FAIL_CLOSED_RECOVERY = "Fix or remove the rejected `checks` entries named above in vinaya.config.json, then re-run `vinaya check`. Until every entry resolves, NO check runs — vinaya refuses the whole run rather than executing a partial ruleset. `vinaya check --plan` prints the same resolution, and `vinaya doctor` carries the permanent diagnostic for each rejected entry.";
|
|
3765
|
+
function refusal(message) {
|
|
3766
|
+
return { schema: 1, check: "config", severity: "error", message, agent_recovery_prompt: FAIL_CLOSED_RECOVERY };
|
|
3767
|
+
}
|
|
3768
|
+
function resolveForRun(configResult) {
|
|
3769
|
+
const base = configResult.ok ? resolveChecks(coreCheckRegistry(), configResult.config?.checks) : {
|
|
3770
|
+
resolved: resolveChecks(coreCheckRegistry(), undefined).resolved,
|
|
3549
3771
|
failures: [{ key: configResult.path, reason: `invalid \`checks\` registration — ${configResult.error}` }]
|
|
3550
3772
|
};
|
|
3773
|
+
const duplicates = duplicateIdFailures(base.resolved);
|
|
3774
|
+
const refusals = [
|
|
3775
|
+
...base.failures.map((failure) => configResult.ok ? refusal(bareKeyRejectedDiagnostic(failure.key)) : refusal(`${failure.key}: ${failure.reason} — refusing to run any check.`)),
|
|
3776
|
+
...duplicates.map((failure) => refusal(`${failure.key}: ${failure.reason} — refusing to run any check.`))
|
|
3777
|
+
];
|
|
3778
|
+
return { result: { resolved: base.resolved, failures: [...base.failures, ...duplicates] }, refusals };
|
|
3551
3779
|
}
|
|
3552
|
-
function
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
process.stdout.write(`${bareKeyNextMinorWarning(failure.key)}
|
|
3561
|
-
`);
|
|
3562
|
-
}
|
|
3780
|
+
function substitutionNotices(resolved) {
|
|
3781
|
+
return resolved.filter((entry2) => entry2.state === "overridden").map((entry2) => ({
|
|
3782
|
+
schema: 1,
|
|
3783
|
+
check: "config",
|
|
3784
|
+
severity: "warning",
|
|
3785
|
+
message: overriddenReplacesCoreDiagnostic(entry2.name),
|
|
3786
|
+
agent_recovery_prompt: `If replacing the core check "${entry2.name}" was intended, nothing to do — this is a notice, not a failure. If it was not, remove or rename that \`checks\` entry in vinaya.config.json so the core check runs again.`
|
|
3787
|
+
}));
|
|
3563
3788
|
}
|
|
3564
3789
|
async function checkCommand(args) {
|
|
3565
3790
|
const jsonOutput = args.includes("--json");
|
|
@@ -3570,8 +3795,8 @@ async function checkCommand(args) {
|
|
|
3570
3795
|
const planRequested = args.includes("--plan");
|
|
3571
3796
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
3572
3797
|
const requestedName = positional[0];
|
|
3798
|
+
const { result, refusals } = resolveForRun(loadConfigChecked());
|
|
3573
3799
|
if (planRequested) {
|
|
3574
|
-
const result = resolveForPlan(loadConfigChecked());
|
|
3575
3800
|
if (jsonOutput) {
|
|
3576
3801
|
process.stdout.write(`${JSON.stringify(renderPlanJson(result), null, 2)}
|
|
3577
3802
|
`);
|
|
@@ -3579,18 +3804,50 @@ async function checkCommand(args) {
|
|
|
3579
3804
|
process.stdout.write(`${renderPlanTable(result)}
|
|
3580
3805
|
`);
|
|
3581
3806
|
}
|
|
3582
|
-
process.
|
|
3807
|
+
process.exitCode = result.failures.length > 0 ? 1 : 0;
|
|
3808
|
+
return;
|
|
3583
3809
|
}
|
|
3584
3810
|
if (!allRequested && !requestedName) {
|
|
3585
3811
|
console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--local] [--parallel[=n]]");
|
|
3586
|
-
process.
|
|
3812
|
+
process.exitCode = 2;
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
if (refusals.length > 0) {
|
|
3816
|
+
const outcome = {
|
|
3817
|
+
name: "config",
|
|
3818
|
+
status: "error",
|
|
3819
|
+
exitCode: null,
|
|
3820
|
+
errors: refusals,
|
|
3821
|
+
durationMs: 0
|
|
3822
|
+
};
|
|
3823
|
+
for (const e of refusals)
|
|
3824
|
+
emitCheckError(e);
|
|
3825
|
+
if (jsonOutput) {
|
|
3826
|
+
printJson({ checks: [outcome] });
|
|
3827
|
+
} else {
|
|
3828
|
+
process.stdout.write(`✗ ${outcome.name}: refused — no checks ran
|
|
3829
|
+
`);
|
|
3830
|
+
for (const e of refusals)
|
|
3831
|
+
process.stdout.write(` ${e.severity}: ${e.message}
|
|
3832
|
+
`);
|
|
3833
|
+
}
|
|
3834
|
+
process.exitCode = 1;
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
const allSpecs = result.resolved.map((entry2) => entry2.spec);
|
|
3838
|
+
const notices = substitutionNotices(result.resolved);
|
|
3839
|
+
for (const notice of notices)
|
|
3840
|
+
emitCheckError(notice);
|
|
3841
|
+
if (!jsonOutput) {
|
|
3842
|
+
for (const notice of notices)
|
|
3843
|
+
process.stdout.write(`⚠ ${notice.message}
|
|
3844
|
+
`);
|
|
3587
3845
|
}
|
|
3588
|
-
const { specs: customSpecs, errorOutcome, checks: configChecks } = customSpecsFromConfig();
|
|
3589
|
-
const allSpecs = [...coreCheckRegistry(), ...customSpecs];
|
|
3590
3846
|
const specsToRun = allRequested ? allSpecs.filter(runsUnderAll) : allSpecs.filter((s) => s.name === requestedName);
|
|
3591
3847
|
if (!allRequested && specsToRun.length === 0) {
|
|
3592
3848
|
console.error(`Unknown check: ${requestedName}`);
|
|
3593
|
-
process.
|
|
3849
|
+
process.exitCode = 2;
|
|
3850
|
+
return;
|
|
3594
3851
|
}
|
|
3595
3852
|
const changed = diffOnly ? changedFiles() : null;
|
|
3596
3853
|
const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
|
|
@@ -3600,15 +3857,14 @@ async function checkCommand(args) {
|
|
|
3600
3857
|
defaultTimeoutMs: 30000,
|
|
3601
3858
|
localOnly
|
|
3602
3859
|
}) : [];
|
|
3603
|
-
const
|
|
3604
|
-
for (const o of allOutcomes) {
|
|
3860
|
+
for (const o of outcomes) {
|
|
3605
3861
|
for (const e of o.errors)
|
|
3606
3862
|
emitCheckError(e);
|
|
3607
3863
|
}
|
|
3608
3864
|
if (jsonOutput) {
|
|
3609
|
-
printJson({ checks:
|
|
3865
|
+
printJson({ checks: outcomes });
|
|
3610
3866
|
} else {
|
|
3611
|
-
for (const o of
|
|
3867
|
+
for (const o of outcomes) {
|
|
3612
3868
|
const symbol = o.status === "pass" ? "✓" : o.status === "skipped" ? "·" : "✗";
|
|
3613
3869
|
process.stdout.write(`${symbol} ${o.name}: ${o.status} (${Math.round(o.durationMs)}ms)
|
|
3614
3870
|
`);
|
|
@@ -3616,21 +3872,20 @@ async function checkCommand(args) {
|
|
|
3616
3872
|
process.stdout.write(` ${e.severity}: ${e.message}
|
|
3617
3873
|
`);
|
|
3618
3874
|
}
|
|
3619
|
-
printClassificationWarnings(configChecks);
|
|
3620
3875
|
}
|
|
3621
|
-
const failed =
|
|
3622
|
-
process.
|
|
3876
|
+
const failed = outcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
|
|
3877
|
+
process.exitCode = failed ? 1 : 0;
|
|
3623
3878
|
}
|
|
3624
3879
|
|
|
3625
3880
|
// src/commands/demo.ts
|
|
3626
|
-
import { execFileSync as
|
|
3881
|
+
import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
|
|
3627
3882
|
import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3628
3883
|
import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
|
|
3629
3884
|
var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
|
|
3630
3885
|
var FIXTURE_PATH = ".vinaya-demo-brief.md";
|
|
3631
3886
|
var STATE_FILENAME = "vinaya-demo-state.json";
|
|
3632
3887
|
function gitCapture(repoRoot, args) {
|
|
3633
|
-
return
|
|
3888
|
+
return execFileSync7("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3634
3889
|
}
|
|
3635
3890
|
function currentBranchName(repoRoot) {
|
|
3636
3891
|
try {
|
|
@@ -3641,7 +3896,7 @@ function currentBranchName(repoRoot) {
|
|
|
3641
3896
|
}
|
|
3642
3897
|
function branchExists(repoRoot, name) {
|
|
3643
3898
|
try {
|
|
3644
|
-
|
|
3899
|
+
execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
|
|
3645
3900
|
cwd: repoRoot,
|
|
3646
3901
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3647
3902
|
});
|
|
@@ -3843,7 +4098,8 @@ async function demoBreakCommand(args) {
|
|
|
3843
4098
|
}
|
|
3844
4099
|
|
|
3845
4100
|
// src/commands/doctor.ts
|
|
3846
|
-
import {
|
|
4101
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
4102
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
3847
4103
|
import { join as join9 } from "node:path";
|
|
3848
4104
|
|
|
3849
4105
|
// src/lib/self-host.ts
|
|
@@ -4124,13 +4380,94 @@ function diagnoseCustomChecks(repoRoot, config) {
|
|
|
4124
4380
|
}
|
|
4125
4381
|
return findings;
|
|
4126
4382
|
}
|
|
4383
|
+
function listTrackedFiles(repoRoot) {
|
|
4384
|
+
try {
|
|
4385
|
+
return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
|
|
4386
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
4387
|
+
} catch {
|
|
4388
|
+
return [];
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
function diagnoseDocOwnersHealth(repoRoot) {
|
|
4392
|
+
const path = join9(repoRoot, DOC_OWNERS_PATH);
|
|
4393
|
+
const content = existsSync8(path) ? readFileSync8(path, "utf-8") : null;
|
|
4394
|
+
const state = classifyDocOwnersManifest(content);
|
|
4395
|
+
if (state === "absent" || state === "empty")
|
|
4396
|
+
return [];
|
|
4397
|
+
const { bindings } = parseDocOwners(content);
|
|
4398
|
+
if (bindings.length === 0)
|
|
4399
|
+
return [];
|
|
4400
|
+
const codeFiles = listTrackedFiles(repoRoot).filter(isCodeFile);
|
|
4401
|
+
const findings = [];
|
|
4402
|
+
for (const b of bindings) {
|
|
4403
|
+
let flagged = false;
|
|
4404
|
+
const re = globToRegex(b.glob);
|
|
4405
|
+
if (!codeFiles.some((f) => re.test(f))) {
|
|
4406
|
+
flagged = true;
|
|
4407
|
+
findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} binds glob '${b.glob}', which matches none of the ${codeFiles.length} ` + "tracked code file(s) in this repo — the code it names may have been deleted, renamed, or never " + "existed. Repoint the binding to the code's new location, or remove it."));
|
|
4408
|
+
}
|
|
4409
|
+
if (!isUrlPointer(b.pointer)) {
|
|
4410
|
+
const pointerPath = join9(repoRoot, pointerToPath(b.pointer));
|
|
4411
|
+
if (!existsSync8(pointerPath)) {
|
|
4412
|
+
flagged = true;
|
|
4413
|
+
findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Repoint the ` + "binding, or add the missing doc."));
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
if (!flagged) {
|
|
4417
|
+
findings.push(ok("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} — '${b.glob}' → ${b.pointer} is live.`));
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
return findings;
|
|
4421
|
+
}
|
|
4422
|
+
var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
|
|
4423
|
+
function readWorkspacesForDoctor(repoRoot) {
|
|
4424
|
+
const fromPackageJson = () => {
|
|
4425
|
+
try {
|
|
4426
|
+
const pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
|
|
4427
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
4428
|
+
} catch {
|
|
4429
|
+
return [];
|
|
4430
|
+
}
|
|
4431
|
+
};
|
|
4432
|
+
const fromPnpmWorkspaceYaml = () => {
|
|
4433
|
+
try {
|
|
4434
|
+
return parsePnpmWorkspaceYaml(readFileSync8(join9(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
|
|
4435
|
+
} catch {
|
|
4436
|
+
return [];
|
|
4437
|
+
}
|
|
4438
|
+
};
|
|
4439
|
+
return [...fromPackageJson(), ...fromPnpmWorkspaceYaml()];
|
|
4440
|
+
}
|
|
4441
|
+
function listChildDirsForDoctor(dir, repoRoot) {
|
|
4442
|
+
try {
|
|
4443
|
+
return readdirSync3(join9(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4444
|
+
} catch {
|
|
4445
|
+
return [];
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
function diagnoseBlastRadiusDeprecation(repoRoot, config) {
|
|
4449
|
+
const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
|
|
4450
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync8(join9(repoRoot, p)));
|
|
4451
|
+
const covered = new Set([...derived, ...defaults]);
|
|
4452
|
+
const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
|
|
4453
|
+
const legacyPath = join9(repoRoot, LEGACY_AEG_PACKAGES_PATH);
|
|
4454
|
+
if (!existsSync8(legacyPath)) {
|
|
4455
|
+
return [
|
|
4456
|
+
info("blast-radius", `checkBlastRadiusScope is active via live derivation (${derived.length} packages/* domain(s)) + built-in defaults (${defaults.length} present) — no ${LEGACY_AEG_PACKAGES_PATH}, and the check is not dormant.`)
|
|
4457
|
+
];
|
|
4458
|
+
}
|
|
4459
|
+
const legacyEntries = readFileSync8(legacyPath, "utf-8").split(`
|
|
4460
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
4461
|
+
const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
|
|
4462
|
+
const migrationNote = uncovered.length === 0 ? "every entry is already covered by live derivation, the built-in defaults, or vinaya.config.json blastRadius.extraDomains — the file can be deleted." : `migrate ${uncovered.length} entr${uncovered.length === 1 ? "y" : "ies"} not yet covered before deleting it: ${uncovered.join(", ")}.`;
|
|
4463
|
+
return [
|
|
4464
|
+
warn("blast-radius", `${LEGACY_AEG_PACKAGES_PATH} is deprecated — checkBlastRadiusScope now derives packages/* domains live and ships built-in cross-cutting defaults. Declare anything beyond those in vinaya.config.json's blastRadius.extraDomains instead. ${migrationNote}`)
|
|
4465
|
+
];
|
|
4466
|
+
}
|
|
4127
4467
|
function diagnoseEnvDeclarations(repoRoot, config) {
|
|
4128
|
-
const
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
run: join9(repoRoot, entry2.run)
|
|
4132
|
-
}));
|
|
4133
|
-
const missing = checksMissingEnvDeclaration([...coreCheckRegistry(), ...customSpecs]);
|
|
4468
|
+
const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
|
|
4469
|
+
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join9(repoRoot, entry2.spec.run) } : entry2.spec);
|
|
4470
|
+
const missing = checksMissingEnvDeclaration(specs);
|
|
4134
4471
|
const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
|
|
4135
4472
|
for (const message of lintEnvDeclarations(config?.checks)) {
|
|
4136
4473
|
findings.push(warn("env", message));
|
|
@@ -4142,10 +4479,10 @@ function diagnoseCheckClassification(config) {
|
|
|
4142
4479
|
const findings = [];
|
|
4143
4480
|
for (const entry2 of classification.resolved) {
|
|
4144
4481
|
if (entry2.state === "overridden")
|
|
4145
|
-
findings.push(warn("checks",
|
|
4482
|
+
findings.push(warn("checks", overriddenReplacesCoreDiagnostic(entry2.name)));
|
|
4146
4483
|
}
|
|
4147
4484
|
for (const failure of classification.failures) {
|
|
4148
|
-
findings.push(
|
|
4485
|
+
findings.push(error("checks", bareKeyRejectedDiagnostic(failure.key)));
|
|
4149
4486
|
}
|
|
4150
4487
|
return findings;
|
|
4151
4488
|
}
|
|
@@ -4187,6 +4524,29 @@ async function diagnoseBranchProtection(deps, owner, repo) {
|
|
|
4187
4524
|
}
|
|
4188
4525
|
return info("branch-protection", "main branch protection could not be determined (no gh auth, no remote, or a permission gap).");
|
|
4189
4526
|
}
|
|
4527
|
+
var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
|
|
4528
|
+
function diagnoseTestCi(repoRoot) {
|
|
4529
|
+
let pkg;
|
|
4530
|
+
try {
|
|
4531
|
+
pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
|
|
4532
|
+
} catch {
|
|
4533
|
+
return [];
|
|
4534
|
+
}
|
|
4535
|
+
const testScript = pkg?.scripts?.test;
|
|
4536
|
+
if (typeof testScript !== "string" || testScript.trim() === "")
|
|
4537
|
+
return [];
|
|
4538
|
+
const workflowsDir = join9(repoRoot, ".github/workflows");
|
|
4539
|
+
const files = existsSync8(workflowsDir) ? readdirSync3(workflowsDir).filter((name) => statSync2(join9(workflowsDir, name)).isFile()) : [];
|
|
4540
|
+
const invoked = files.some((name) => {
|
|
4541
|
+
const content = readFileSync8(join9(workflowsDir, name), "utf-8");
|
|
4542
|
+
return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
|
|
4543
|
+
});
|
|
4544
|
+
if (invoked)
|
|
4545
|
+
return [];
|
|
4546
|
+
return [
|
|
4547
|
+
warn("test-ci", "no workflow under .github/workflows/ appears to invoke this repo's `package.json` test script — " + "vinaya requires a Test Plan on every PR and enforces it as a blocking gate, but nothing here verifies that tests actually run.")
|
|
4548
|
+
];
|
|
4549
|
+
}
|
|
4190
4550
|
function symbolFor(severity) {
|
|
4191
4551
|
switch (severity) {
|
|
4192
4552
|
case "ok":
|
|
@@ -4240,12 +4600,15 @@ async function runDoctor(args, deps) {
|
|
|
4240
4600
|
hasDrift = install.hasDrift;
|
|
4241
4601
|
findings.push(...await diagnoseHookRouting(repo.repoRoot, hookDir, deps.readHooksPath));
|
|
4242
4602
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
4603
|
+
findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
|
|
4243
4604
|
}
|
|
4605
|
+
findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
4244
4606
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
4245
4607
|
findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
|
|
4246
4608
|
findings.push(...diagnoseGlobalConfigChecks());
|
|
4247
4609
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
4248
4610
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
4611
|
+
findings.push(...diagnoseTestCi(repo.repoRoot));
|
|
4249
4612
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
4250
4613
|
if (jsonOutput) {
|
|
4251
4614
|
printJson({ healthy, findings });
|
|
@@ -4259,7 +4622,7 @@ async function doctorCommand(args) {
|
|
|
4259
4622
|
}
|
|
4260
4623
|
|
|
4261
4624
|
// src/commands/doctrine.ts
|
|
4262
|
-
import { existsSync as existsSync9 } from "node:fs";
|
|
4625
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
|
|
4263
4626
|
import { dirname as dirname4, join as join10, sep as sep3 } from "node:path";
|
|
4264
4627
|
var ENTRY_SEGMENTS = ["skills", "aeg", "SKILL.md"];
|
|
4265
4628
|
function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
|
|
@@ -4272,6 +4635,12 @@ function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
|
|
|
4272
4635
|
}
|
|
4273
4636
|
return null;
|
|
4274
4637
|
}
|
|
4638
|
+
function listRoleNames(root) {
|
|
4639
|
+
const rolesDir = join10(root, "roles");
|
|
4640
|
+
if (!existsSync9(rolesDir))
|
|
4641
|
+
return [];
|
|
4642
|
+
return readdirSync4(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).sort();
|
|
4643
|
+
}
|
|
4275
4644
|
function doctrineCommand(args) {
|
|
4276
4645
|
const root = resolveDoctrineRoot();
|
|
4277
4646
|
if (root === null) {
|
|
@@ -4279,6 +4648,24 @@ function doctrineCommand(args) {
|
|
|
4279
4648
|
`);
|
|
4280
4649
|
process.exit(1);
|
|
4281
4650
|
}
|
|
4651
|
+
const roleFlagIndex = args.indexOf("--role");
|
|
4652
|
+
if (roleFlagIndex !== -1) {
|
|
4653
|
+
const roleName = args[roleFlagIndex + 1];
|
|
4654
|
+
const validRoleNames = listRoleNames(root);
|
|
4655
|
+
if (roleName === undefined || roleName.startsWith("--") || !validRoleNames.includes(roleName)) {
|
|
4656
|
+
process.stderr.write(`vinaya doctrine --role: '${roleName ?? ""}' is not a known role. ` + `Valid role names: ${validRoleNames.join(", ")}
|
|
4657
|
+
`);
|
|
4658
|
+
process.exit(1);
|
|
4659
|
+
}
|
|
4660
|
+
const entry3 = join10(root, "roles", `${roleName}.md`);
|
|
4661
|
+
if (args.includes("--json")) {
|
|
4662
|
+
printJson({ root, entry: entry3 });
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
process.stdout.write(`${entry3}
|
|
4666
|
+
`);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4282
4669
|
const entry2 = join10(root, ...ENTRY_SEGMENTS);
|
|
4283
4670
|
if (args.includes("--json")) {
|
|
4284
4671
|
printJson({ root, entry: entry2 });
|
|
@@ -4695,7 +5082,7 @@ async function initProductCommand(args) {
|
|
|
4695
5082
|
}
|
|
4696
5083
|
|
|
4697
5084
|
// src/commands/issue.ts
|
|
4698
|
-
import { execFileSync as
|
|
5085
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
4699
5086
|
|
|
4700
5087
|
// src/lib/forge-write.ts
|
|
4701
5088
|
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
@@ -4918,7 +5305,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
|
|
|
4918
5305
|
function fetchForgeLabels(issueRef) {
|
|
4919
5306
|
let out;
|
|
4920
5307
|
try {
|
|
4921
|
-
out =
|
|
5308
|
+
out = execFileSync9("gh", ["issue", "view", issueRef, "--json", "labels"], {
|
|
4922
5309
|
encoding: "utf8",
|
|
4923
5310
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4924
5311
|
});
|
|
@@ -4946,7 +5333,7 @@ function reportPass(json, command) {
|
|
|
4946
5333
|
function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
|
|
4947
5334
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
4948
5335
|
try {
|
|
4949
|
-
const out =
|
|
5336
|
+
const out = execFileSync9("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
4950
5337
|
const url = out.trim();
|
|
4951
5338
|
if (json)
|
|
4952
5339
|
printJson({ validated: true, written: true, url });
|
|
@@ -5021,17 +5408,27 @@ import { chmodSync as chmodSync2, existsSync as existsSync13, mkdirSync as mkdir
|
|
|
5021
5408
|
import { join as join15 } from "node:path";
|
|
5022
5409
|
var TEMPLATE_PATH = join15(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
5023
5410
|
var CHECKS_DIR = join15("scripts", "vinaya-checks");
|
|
5024
|
-
var
|
|
5411
|
+
var USAGE = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
|
|
5025
5412
|
function newCheckCommand(args) {
|
|
5026
5413
|
const name = args[0];
|
|
5027
|
-
if (!name
|
|
5028
|
-
console.error(
|
|
5414
|
+
if (!name) {
|
|
5415
|
+
console.error(USAGE);
|
|
5416
|
+
process.exit(2);
|
|
5417
|
+
}
|
|
5418
|
+
if (coreCheckRegistry().some((spec) => spec.name === name)) {
|
|
5419
|
+
console.error(`Refusing: "${name}" is a core check id. Registering it under \`checks\` REPLACES that core check — the core one stops running — so a scaffolded stub is never the right shape for it. Pick a namespaced name, or write the override entry by hand if you really mean to replace the core check.`);
|
|
5420
|
+
process.exit(2);
|
|
5421
|
+
}
|
|
5422
|
+
if (!isValidNamespacedKey(name)) {
|
|
5423
|
+
console.error(`Refusing: "${name}" is not a valid check id. ${USAGE.slice("Usage: ".length)}
|
|
5424
|
+
\`vinaya check\` refuses the entire run over a key it cannot resolve, so a bare, un-namespaced name would brick every check invocation in this repo.`);
|
|
5029
5425
|
process.exit(2);
|
|
5030
5426
|
}
|
|
5031
5427
|
const scriptsDir = join15(process.cwd(), CHECKS_DIR);
|
|
5032
5428
|
if (!existsSync13(scriptsDir))
|
|
5033
5429
|
mkdirSync4(scriptsDir, { recursive: true });
|
|
5034
|
-
const
|
|
5430
|
+
const fileStem = name.slice(name.indexOf("/") + 1);
|
|
5431
|
+
const targetPath = join15(scriptsDir, `${fileStem}.ts`);
|
|
5035
5432
|
if (existsSync13(targetPath)) {
|
|
5036
5433
|
console.error(`Error: ${targetPath} already exists.`);
|
|
5037
5434
|
process.exit(1);
|
|
@@ -5040,7 +5437,7 @@ function newCheckCommand(args) {
|
|
|
5040
5437
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
5041
5438
|
writeFileSync7(targetPath, contents, "utf-8");
|
|
5042
5439
|
chmodSync2(targetPath, 493);
|
|
5043
|
-
const relPath = join15(CHECKS_DIR, `${
|
|
5440
|
+
const relPath = join15(CHECKS_DIR, `${fileStem}.ts`);
|
|
5044
5441
|
const registration = JSON.stringify({ checks: { [name]: { run: `./${relPath}`, scope: "diff" } } }, null, 2);
|
|
5045
5442
|
process.stdout.write(`Created ${relPath}
|
|
5046
5443
|
|
|
@@ -5050,12 +5447,12 @@ ${registration}
|
|
|
5050
5447
|
}
|
|
5051
5448
|
|
|
5052
5449
|
// src/commands/pr.ts
|
|
5053
|
-
import { execFileSync as
|
|
5450
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
5054
5451
|
var RETRY_CREATE2 = "vinaya pr create --validate-only …";
|
|
5055
5452
|
var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
|
|
5056
5453
|
function git2(args) {
|
|
5057
5454
|
try {
|
|
5058
|
-
return
|
|
5455
|
+
return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5059
5456
|
} catch {
|
|
5060
5457
|
return "";
|
|
5061
5458
|
}
|
|
@@ -5089,7 +5486,7 @@ function reportPass2(json, command) {
|
|
|
5089
5486
|
function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
5090
5487
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
5091
5488
|
try {
|
|
5092
|
-
const out =
|
|
5489
|
+
const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
5093
5490
|
const url = out.trim();
|
|
5094
5491
|
if (json)
|
|
5095
5492
|
printJson({ validated: true, written: true, url });
|
|
@@ -5103,7 +5500,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
|
5103
5500
|
function fetchPrForgeContext(prRef) {
|
|
5104
5501
|
let viewOut;
|
|
5105
5502
|
try {
|
|
5106
|
-
viewOut =
|
|
5503
|
+
viewOut = execFileSync10("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
|
|
5107
5504
|
encoding: "utf8",
|
|
5108
5505
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5109
5506
|
});
|
|
@@ -5204,14 +5601,193 @@ function prEditCommand(args) {
|
|
|
5204
5601
|
runGhWrite2(["pr", "edit", prRef], ghArgs, bodyResult, json);
|
|
5205
5602
|
}
|
|
5206
5603
|
|
|
5604
|
+
// src/commands/pr-report.ts
|
|
5605
|
+
import { execFileSync as execFileSync11, spawnSync as spawnSync2 } from "node:child_process";
|
|
5606
|
+
import { existsSync as existsSync14, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5607
|
+
import { join as join16 } from "node:path";
|
|
5608
|
+
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
5609
|
+
var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
|
|
5610
|
+
function git3(args) {
|
|
5611
|
+
try {
|
|
5612
|
+
return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5613
|
+
} catch {
|
|
5614
|
+
return "";
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
|
|
5618
|
+
class GitCommandError extends Error {
|
|
5619
|
+
args;
|
|
5620
|
+
cause;
|
|
5621
|
+
constructor(args, cause) {
|
|
5622
|
+
super(`\`git ${args.join(" ")}\` failed: ${cause}`);
|
|
5623
|
+
this.args = args;
|
|
5624
|
+
this.cause = cause;
|
|
5625
|
+
this.name = "GitCommandError";
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
function gitStrict(args) {
|
|
5629
|
+
try {
|
|
5630
|
+
return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5631
|
+
} catch (err) {
|
|
5632
|
+
const stderr = err.stderr;
|
|
5633
|
+
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
class UnresolvableMergeBaseError extends Error {
|
|
5638
|
+
head;
|
|
5639
|
+
triedRefs;
|
|
5640
|
+
constructor(head, triedRefs) {
|
|
5641
|
+
super(`could not resolve a merge-base for HEAD (${head}) against any of: ${triedRefs.join(", ")}. ` + `This repo's default branch may not be named "main", or "origin" may not be configured — set BASE_SHA to override.`);
|
|
5642
|
+
this.head = head;
|
|
5643
|
+
this.triedRefs = triedRefs;
|
|
5644
|
+
this.name = "UnresolvableMergeBaseError";
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
function resolveMergeBase(head) {
|
|
5648
|
+
const primary = process.env.BASE_SHA || "origin/main";
|
|
5649
|
+
const tried = primary === "main" ? [primary] : [primary, "main"];
|
|
5650
|
+
for (const ref of tried) {
|
|
5651
|
+
const base = git3(["merge-base", ref, head]);
|
|
5652
|
+
if (base)
|
|
5653
|
+
return base;
|
|
5654
|
+
}
|
|
5655
|
+
throw new UnresolvableMergeBaseError(head, tried);
|
|
5656
|
+
}
|
|
5657
|
+
function computeGroupA() {
|
|
5658
|
+
const head = gitStrict(["rev-parse", "HEAD"]);
|
|
5659
|
+
const base = resolveMergeBase(head);
|
|
5660
|
+
const numstat = gitStrict(["diff", `${base}...${head}`, "--numstat"]);
|
|
5661
|
+
return { head, base, numstat };
|
|
5662
|
+
}
|
|
5663
|
+
function resolveSelfEntry() {
|
|
5664
|
+
const root = packageRoot(import.meta.url);
|
|
5665
|
+
const dist = join16(root, "dist", "index.js");
|
|
5666
|
+
return existsSync14(dist) ? dist : join16(root, "src", "index.ts");
|
|
5667
|
+
}
|
|
5668
|
+
var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
|
|
5669
|
+
function anyGateFailed(outcomes) {
|
|
5670
|
+
return outcomes.some((o) => FAILING_STATUSES.has(o.status));
|
|
5671
|
+
}
|
|
5672
|
+
function runRealGates() {
|
|
5673
|
+
const entry2 = resolveSelfEntry();
|
|
5674
|
+
const proc = spawnSync2(process.execPath, [entry2, "check", "--all", "--diff-only", "--json"], {
|
|
5675
|
+
cwd: process.cwd(),
|
|
5676
|
+
encoding: "utf8",
|
|
5677
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
5678
|
+
maxBuffer: 32 * 1024 * 1024
|
|
5679
|
+
});
|
|
5680
|
+
const stdout = proc.stdout ?? "";
|
|
5681
|
+
try {
|
|
5682
|
+
const parsed = JSON.parse(stdout);
|
|
5683
|
+
const outcomes = parsed.data.checks;
|
|
5684
|
+
return { outcomes, failed: anyGateFailed(outcomes) };
|
|
5685
|
+
} catch {
|
|
5686
|
+
return {
|
|
5687
|
+
outcomes: [
|
|
5688
|
+
{
|
|
5689
|
+
name: "check",
|
|
5690
|
+
status: "error",
|
|
5691
|
+
errors: [
|
|
5692
|
+
{ severity: "error", message: "`vinaya check --all --diff-only --json` did not produce parseable output." }
|
|
5693
|
+
]
|
|
5694
|
+
}
|
|
5695
|
+
],
|
|
5696
|
+
failed: true
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
function renderGroupA(groupA) {
|
|
5701
|
+
return [
|
|
5702
|
+
"### Group A — recomputable",
|
|
5703
|
+
"",
|
|
5704
|
+
`\`git diff ${groupA.base}...${groupA.head} --numstat\``,
|
|
5705
|
+
"",
|
|
5706
|
+
"```",
|
|
5707
|
+
groupA.numstat,
|
|
5708
|
+
"```"
|
|
5709
|
+
].join(`
|
|
5710
|
+
`);
|
|
5711
|
+
}
|
|
5712
|
+
function renderGroupB(outcomes) {
|
|
5713
|
+
const sorted = [...outcomes].sort((a, b) => a.name.localeCompare(b.name));
|
|
5714
|
+
const lines = sorted.flatMap((o) => {
|
|
5715
|
+
const rows = [`${o.name}: ${o.status}`];
|
|
5716
|
+
for (const e of o.errors)
|
|
5717
|
+
rows.push(` ${e.severity}: ${e.message}`);
|
|
5718
|
+
return rows;
|
|
5719
|
+
});
|
|
5720
|
+
return ["### Group B — attested", "", "`vinaya check --all --diff-only`", "", "```", lines.join(`
|
|
5721
|
+
`), "```"].join(`
|
|
5722
|
+
`);
|
|
5723
|
+
}
|
|
5724
|
+
function buildBlockInner(groupA, gateOutcomes) {
|
|
5725
|
+
return [`Head: ${groupA.head}`, "", renderGroupA(groupA), "", renderGroupB(gateOutcomes)].join(`
|
|
5726
|
+
`);
|
|
5727
|
+
}
|
|
5728
|
+
function replaceEvidenceBlock(body, blockInner) {
|
|
5729
|
+
const full = `${EVIDENCE_START}
|
|
5730
|
+
${blockInner}
|
|
5731
|
+
${EVIDENCE_END}`;
|
|
5732
|
+
const bounds = anchoredRegionBounds(body, "EVIDENCE");
|
|
5733
|
+
if (bounds) {
|
|
5734
|
+
return `${body.slice(0, bounds.outerStart)}${full}${body.slice(bounds.outerEnd)}`;
|
|
5735
|
+
}
|
|
5736
|
+
const sep4 = body.length === 0 || body.endsWith(`
|
|
5737
|
+
`) ? "" : `
|
|
5738
|
+
`;
|
|
5739
|
+
return `${body}${sep4}
|
|
5740
|
+
${full}
|
|
5741
|
+
`;
|
|
5742
|
+
}
|
|
5743
|
+
async function buildReport(opts = {}) {
|
|
5744
|
+
const groupA = opts.groupA ?? computeGroupA();
|
|
5745
|
+
const gateRunner = opts.gateRunner ?? runRealGates;
|
|
5746
|
+
const gateResult = await gateRunner();
|
|
5747
|
+
const blockInner = buildBlockInner(groupA, gateResult.outcomes);
|
|
5748
|
+
const block = `${EVIDENCE_START}
|
|
5749
|
+
${blockInner}
|
|
5750
|
+
${EVIDENCE_END}`;
|
|
5751
|
+
return { block, blockInner, gatesFailed: gateResult.failed, gateOutcomes: gateResult.outcomes };
|
|
5752
|
+
}
|
|
5753
|
+
async function prReportCommand(args) {
|
|
5754
|
+
const writeIdx = args.indexOf("--write");
|
|
5755
|
+
const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
|
|
5756
|
+
if (writeIdx !== -1 && !writePath) {
|
|
5757
|
+
console.error("Usage: vinaya pr report [--write <body-file>]");
|
|
5758
|
+
process.exit(2);
|
|
5759
|
+
}
|
|
5760
|
+
let result;
|
|
5761
|
+
try {
|
|
5762
|
+
result = await buildReport();
|
|
5763
|
+
} catch (err) {
|
|
5764
|
+
if (err instanceof UnresolvableMergeBaseError || err instanceof GitCommandError) {
|
|
5765
|
+
console.error(`vinaya pr report: refused — ${err.message}`);
|
|
5766
|
+
process.exit(1);
|
|
5767
|
+
}
|
|
5768
|
+
throw err;
|
|
5769
|
+
}
|
|
5770
|
+
if (writePath) {
|
|
5771
|
+
const existing = existsSync14(writePath) ? readFileSync14(writePath, "utf8") : "";
|
|
5772
|
+
const updated = replaceEvidenceBlock(existing, result.blockInner);
|
|
5773
|
+
writeFileSync8(writePath, updated);
|
|
5774
|
+
process.stdout.write(`Wrote AEG:EVIDENCE block to ${writePath}
|
|
5775
|
+
`);
|
|
5776
|
+
} else {
|
|
5777
|
+
process.stdout.write(`${result.block}
|
|
5778
|
+
`);
|
|
5779
|
+
}
|
|
5780
|
+
process.exit(result.gatesFailed ? 1 : 0);
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5207
5783
|
// src/commands/quickstart.ts
|
|
5208
|
-
import { execFileSync as
|
|
5209
|
-
import { existsSync as
|
|
5210
|
-
import { join as
|
|
5784
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
5785
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
|
|
5786
|
+
import { join as join18 } from "node:path";
|
|
5211
5787
|
|
|
5212
5788
|
// src/lib/doc-owners-write.ts
|
|
5213
|
-
import { existsSync as
|
|
5214
|
-
import { dirname as dirname6, join as
|
|
5789
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "node:fs";
|
|
5790
|
+
import { dirname as dirname6, join as join17 } from "node:path";
|
|
5215
5791
|
function bindingLine(glob, pointer) {
|
|
5216
5792
|
return `${glob} ${pointer}`;
|
|
5217
5793
|
}
|
|
@@ -5227,23 +5803,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
|
|
|
5227
5803
|
`;
|
|
5228
5804
|
}
|
|
5229
5805
|
function planDocOwnersBinding(repoRoot, glob, pointer) {
|
|
5230
|
-
const abs2 =
|
|
5806
|
+
const abs2 = join17(repoRoot, DOC_OWNERS_PATH);
|
|
5231
5807
|
const line = bindingLine(glob, pointer);
|
|
5232
|
-
if (!
|
|
5808
|
+
if (!existsSync15(abs2)) {
|
|
5233
5809
|
return { action: "create-host", line, path: DOC_OWNERS_PATH };
|
|
5234
5810
|
}
|
|
5235
|
-
const existing =
|
|
5811
|
+
const existing = readFileSync15(abs2, "utf-8");
|
|
5236
5812
|
const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
|
|
5237
5813
|
return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
|
|
5238
5814
|
}
|
|
5239
5815
|
function applyDocOwnersBinding(repoRoot, plan, glob, pointer) {
|
|
5240
|
-
const abs2 =
|
|
5816
|
+
const abs2 = join17(repoRoot, DOC_OWNERS_PATH);
|
|
5241
5817
|
if (plan.action === "create-host") {
|
|
5242
5818
|
mkdirSync5(dirname6(abs2), { recursive: true });
|
|
5243
|
-
|
|
5819
|
+
writeFileSync9(abs2, freshDocOwners(glob, pointer), "utf-8");
|
|
5244
5820
|
} else if (plan.action === "append-row") {
|
|
5245
|
-
const existing =
|
|
5246
|
-
|
|
5821
|
+
const existing = readFileSync15(abs2, "utf-8");
|
|
5822
|
+
writeFileSync9(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
|
|
5247
5823
|
}
|
|
5248
5824
|
}
|
|
5249
5825
|
function renderDocOwnersBindingDiffLine(plan) {
|
|
@@ -5303,7 +5879,7 @@ function pointerIsMissingLocalFile(repoRoot, pointer) {
|
|
|
5303
5879
|
if (/^https?:\/\//i.test(pointer))
|
|
5304
5880
|
return false;
|
|
5305
5881
|
const path = pointer.split("#")[0] ?? pointer;
|
|
5306
|
-
return !
|
|
5882
|
+
return !existsSync16(join18(repoRoot, path));
|
|
5307
5883
|
}
|
|
5308
5884
|
async function bindDocOwnerLoop(deps, repoRoot) {
|
|
5309
5885
|
let any = false;
|
|
@@ -5370,7 +5946,7 @@ async function registerProjectLoop(deps) {
|
|
|
5370
5946
|
}
|
|
5371
5947
|
}
|
|
5372
5948
|
function readPackageVersion() {
|
|
5373
|
-
const pkg = JSON.parse(
|
|
5949
|
+
const pkg = JSON.parse(readFileSync16(join18(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
5374
5950
|
return pkg.version;
|
|
5375
5951
|
}
|
|
5376
5952
|
function realDeps6() {
|
|
@@ -5401,7 +5977,7 @@ function realDeps6() {
|
|
|
5401
5977
|
};
|
|
5402
5978
|
}
|
|
5403
5979
|
function execGit(repoRoot, args) {
|
|
5404
|
-
return
|
|
5980
|
+
return execFileSync12("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5405
5981
|
}
|
|
5406
5982
|
function errorDetail(err) {
|
|
5407
5983
|
const stderr = err.stderr;
|
|
@@ -5517,11 +6093,331 @@ async function quickstartCommand(args) {
|
|
|
5517
6093
|
process.exit(await runQuickstart(args, realDeps6()));
|
|
5518
6094
|
}
|
|
5519
6095
|
|
|
6096
|
+
// src/commands/review-post.ts
|
|
6097
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
6098
|
+
import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
|
|
6099
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
6100
|
+
import { join as join19 } from "node:path";
|
|
6101
|
+
class FindingsParseError extends Error {
|
|
6102
|
+
}
|
|
6103
|
+
var CODE_REVIEW_SEVERITIES = ["BLOCKER", "MAJOR", "MINOR"];
|
|
6104
|
+
var SECURITY_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
|
|
6105
|
+
function parseFindingsFile(content, allowedSeverities) {
|
|
6106
|
+
const lines = content.split(`
|
|
6107
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
6108
|
+
return lines.map((line, idx) => {
|
|
6109
|
+
const parts = line.split("|");
|
|
6110
|
+
if (parts.length !== 3) {
|
|
6111
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: expected exactly 3 \`|\`-delimited fields (SEVERITY|file:line|description), found ${parts.length}: ${line}`);
|
|
6112
|
+
}
|
|
6113
|
+
const severity = parts[0].trim().toUpperCase();
|
|
6114
|
+
const location = parts[1].trim();
|
|
6115
|
+
const description = parts[2].trim();
|
|
6116
|
+
if (!allowedSeverities.includes(severity)) {
|
|
6117
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: severity "${parts[0]}" is not one of ${allowedSeverities.join("|")}: ${line}`);
|
|
6118
|
+
}
|
|
6119
|
+
if (!location || !description) {
|
|
6120
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: file:line and description must both be non-empty: ${line}`);
|
|
6121
|
+
}
|
|
6122
|
+
return { severity, location, description };
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
6125
|
+
function sortBySeverity(findings, order) {
|
|
6126
|
+
return findings.map((f, i) => ({ f, i })).sort((a, b) => order.indexOf(a.f.severity) - order.indexOf(b.f.severity) || a.i - b.i).map(({ f }) => f);
|
|
6127
|
+
}
|
|
6128
|
+
function renderFindingsSection(findings) {
|
|
6129
|
+
if (findings.length === 0)
|
|
6130
|
+
return "None.";
|
|
6131
|
+
return findings.map((f, i) => `${i + 1}. [${f.severity}] ${f.location} — ${f.description}`).join(`
|
|
6132
|
+
`);
|
|
6133
|
+
}
|
|
6134
|
+
function renderTokensLine(role, roleLabel, input) {
|
|
6135
|
+
return `Tokens: ${input.taskId}: ${role} — ${roleLabel} — ${input.model} — ${input.tokensIn}/${input.tokensOut}/${input.cost}`;
|
|
6136
|
+
}
|
|
6137
|
+
var CODE_REVIEW_VERDICT_TEXT = {
|
|
6138
|
+
APPROVE: "APPROVE",
|
|
6139
|
+
REQUEST_CHANGES: "REQUEST CHANGES"
|
|
6140
|
+
};
|
|
6141
|
+
function renderCodeReviewComment(input) {
|
|
6142
|
+
const sorted = sortBySeverity(input.findings, CODE_REVIEW_SEVERITIES);
|
|
6143
|
+
return [
|
|
6144
|
+
`VERDICT: ${CODE_REVIEW_VERDICT_TEXT[input.verdict]}`,
|
|
6145
|
+
"",
|
|
6146
|
+
`Judged head: ${input.headSha}`,
|
|
6147
|
+
"",
|
|
6148
|
+
`BRIEF CONFORMANCE: ${input.briefConformance}`,
|
|
6149
|
+
`SPEC CONFORMANCE: ${input.specConformance}`,
|
|
6150
|
+
"",
|
|
6151
|
+
"FINDINGS (ordered by severity):",
|
|
6152
|
+
renderFindingsSection(sorted),
|
|
6153
|
+
"",
|
|
6154
|
+
`SCOPE: ${input.scope}`,
|
|
6155
|
+
`TESTS: ${input.tests}`,
|
|
6156
|
+
`DOCS: ${input.docs}`,
|
|
6157
|
+
"",
|
|
6158
|
+
renderTokensLine("review", "Reviewer", input)
|
|
6159
|
+
].join(`
|
|
6160
|
+
`);
|
|
6161
|
+
}
|
|
6162
|
+
function isNoneFoundClaim(value) {
|
|
6163
|
+
return value.trim().toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ") === "none found";
|
|
6164
|
+
}
|
|
6165
|
+
function renderSecurityComment(input) {
|
|
6166
|
+
const sorted = sortBySeverity(input.findings, SECURITY_SEVERITIES);
|
|
6167
|
+
const lines = [
|
|
6168
|
+
`VERDICT: ${input.verdict}`,
|
|
6169
|
+
"",
|
|
6170
|
+
`Judged head: ${input.headSha}`,
|
|
6171
|
+
"",
|
|
6172
|
+
"FINDINGS (ordered by severity):",
|
|
6173
|
+
renderFindingsSection(sorted),
|
|
6174
|
+
"",
|
|
6175
|
+
`CONFIG SCAN: ${input.configScan}`,
|
|
6176
|
+
""
|
|
6177
|
+
];
|
|
6178
|
+
if (input.secretsEvidence !== null) {
|
|
6179
|
+
lines.push("```", input.secretsEvidence, "```", "");
|
|
6180
|
+
}
|
|
6181
|
+
lines.push(`SECRETS: ${input.secrets}`, "", renderTokensLine("security", "Security", input));
|
|
6182
|
+
return lines.join(`
|
|
6183
|
+
`);
|
|
6184
|
+
}
|
|
6185
|
+
function isBoundToHead(extraction, headSha) {
|
|
6186
|
+
if (!extraction.headSha)
|
|
6187
|
+
return false;
|
|
6188
|
+
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
6189
|
+
}
|
|
6190
|
+
function checkExtraction(extraction, expectedValue, headSha) {
|
|
6191
|
+
if (extraction.danglingNote) {
|
|
6192
|
+
return {
|
|
6193
|
+
ok: false,
|
|
6194
|
+
reason: `no clean VERDICT was found on re-fetch (${extraction.danglingNote}) — the posted comment does not match the gate's line-anchored \`VERDICT:\` pattern.`
|
|
6195
|
+
};
|
|
6196
|
+
}
|
|
6197
|
+
if (extraction.value !== expectedValue) {
|
|
6198
|
+
return {
|
|
6199
|
+
ok: false,
|
|
6200
|
+
reason: `re-extraction found VERDICT "${extraction.value}", expected "${expectedValue}" — the posted comment's VERDICT line does not match what this command rendered.`
|
|
6201
|
+
};
|
|
6202
|
+
}
|
|
6203
|
+
if (!extraction.headSha) {
|
|
6204
|
+
return { ok: false, reason: "the winning VERDICT comment carries no `Judged head:` line on re-fetch." };
|
|
6205
|
+
}
|
|
6206
|
+
if (!isBoundToHead(extraction, headSha)) {
|
|
6207
|
+
return {
|
|
6208
|
+
ok: false,
|
|
6209
|
+
reason: `re-extraction found \`Judged head: ${extraction.headSha}\`, which does not cover the resolved head ${headSha}.`
|
|
6210
|
+
};
|
|
6211
|
+
}
|
|
6212
|
+
return { ok: true, reason: "clean" };
|
|
6213
|
+
}
|
|
6214
|
+
function principalBodies(comments, principalAllowlist) {
|
|
6215
|
+
return comments.filter((c) => isPrincipal(c.author, principalAllowlist)).map((c) => c.body);
|
|
6216
|
+
}
|
|
6217
|
+
function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist) {
|
|
6218
|
+
return checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha);
|
|
6219
|
+
}
|
|
6220
|
+
function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist) {
|
|
6221
|
+
return checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha);
|
|
6222
|
+
}
|
|
6223
|
+
function parseFlags(args) {
|
|
6224
|
+
const map = new Map;
|
|
6225
|
+
for (let i = 0;i < args.length; i++) {
|
|
6226
|
+
const a = args[i];
|
|
6227
|
+
if (a.startsWith("--")) {
|
|
6228
|
+
const value = args[i + 1];
|
|
6229
|
+
if (value === undefined || value.startsWith("--")) {
|
|
6230
|
+
map.set(a, "");
|
|
6231
|
+
continue;
|
|
6232
|
+
}
|
|
6233
|
+
map.set(a, value);
|
|
6234
|
+
i++;
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
return map;
|
|
6238
|
+
}
|
|
6239
|
+
function refuseCmd(message, recovery) {
|
|
6240
|
+
refuse([makeCheckError("review-post", message, recovery)]);
|
|
6241
|
+
}
|
|
6242
|
+
function requireFlag(flags2, name) {
|
|
6243
|
+
const v = flags2.get(name);
|
|
6244
|
+
if (v === undefined || v === "") {
|
|
6245
|
+
refuseCmd(`Missing required \`${name}\` flag.`, `Pass \`${name} <value>\`, then re-run \`vinaya review post ...\`.`);
|
|
6246
|
+
}
|
|
6247
|
+
return v;
|
|
6248
|
+
}
|
|
6249
|
+
function requireTokenField(flags2, name) {
|
|
6250
|
+
const v = requireFlag(flags2, name);
|
|
6251
|
+
if (v !== "-" && !/^\d+$/.test(v)) {
|
|
6252
|
+
refuseCmd(`\`${name} ${v}\` is neither a non-negative integer nor \`-\` (unknown).`, `Pass a whole number or \`-\` for ${name}.`);
|
|
6253
|
+
}
|
|
6254
|
+
return v;
|
|
6255
|
+
}
|
|
6256
|
+
function readFindingsFile(path, allowedSeverities) {
|
|
6257
|
+
if (!path)
|
|
6258
|
+
return [];
|
|
6259
|
+
let content;
|
|
6260
|
+
try {
|
|
6261
|
+
content = readFileSync17(path, "utf8");
|
|
6262
|
+
} catch {
|
|
6263
|
+
refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
|
|
6264
|
+
}
|
|
6265
|
+
try {
|
|
6266
|
+
return parseFindingsFile(content, allowedSeverities);
|
|
6267
|
+
} catch (err) {
|
|
6268
|
+
if (err instanceof FindingsParseError) {
|
|
6269
|
+
refuseCmd(err.message, "Fix the malformed line in the findings file, then re-run.");
|
|
6270
|
+
}
|
|
6271
|
+
throw err;
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
function normalizeCodeReviewVerdict(raw) {
|
|
6275
|
+
const v = raw.trim().toUpperCase().replace(/[-\s]+/g, "_");
|
|
6276
|
+
if (v === "APPROVE")
|
|
6277
|
+
return "APPROVE";
|
|
6278
|
+
if (v === "REQUEST_CHANGES")
|
|
6279
|
+
return "REQUEST_CHANGES";
|
|
6280
|
+
refuseCmd(`\`--verdict ${raw}\` is not APPROVE or REQUEST_CHANGES.`, "Pass `--verdict APPROVE` or `--verdict REQUEST_CHANGES`.");
|
|
6281
|
+
}
|
|
6282
|
+
function normalizeSecurityVerdict(raw) {
|
|
6283
|
+
const v = raw.trim().toUpperCase();
|
|
6284
|
+
if (v === "PASS")
|
|
6285
|
+
return "PASS";
|
|
6286
|
+
if (v === "FAIL")
|
|
6287
|
+
return "FAIL";
|
|
6288
|
+
refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
|
|
6289
|
+
}
|
|
6290
|
+
function gh(args) {
|
|
6291
|
+
try {
|
|
6292
|
+
return execFileSync13("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6293
|
+
} catch (err) {
|
|
6294
|
+
const stderr = err.stderr;
|
|
6295
|
+
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
6296
|
+
}
|
|
6297
|
+
}
|
|
6298
|
+
function resolveHeadSha(pr) {
|
|
6299
|
+
let out;
|
|
6300
|
+
try {
|
|
6301
|
+
out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
|
|
6302
|
+
} catch (err) {
|
|
6303
|
+
refuseCmd(`Could not resolve PR ${pr}'s head via \`gh pr view --json headRefOid\`: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes and the PR number is correct, then re-run.");
|
|
6304
|
+
}
|
|
6305
|
+
if (!out) {
|
|
6306
|
+
refuseCmd(`\`gh pr view ${pr} --json headRefOid\` returned no head sha.`, "Confirm PR exists, then re-run.");
|
|
6307
|
+
}
|
|
6308
|
+
return out;
|
|
6309
|
+
}
|
|
6310
|
+
function postComment(pr, body) {
|
|
6311
|
+
const tmp = join19(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
|
|
6312
|
+
writeFileSync10(tmp, body);
|
|
6313
|
+
try {
|
|
6314
|
+
return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
|
|
6315
|
+
} catch (err) {
|
|
6316
|
+
refuseCmd(`Rendered comment failed to post via \`gh pr comment\`: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network, then re-run — nothing was posted.");
|
|
6317
|
+
} finally {
|
|
6318
|
+
rmSync4(tmp, { force: true });
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
function fetchComments(pr) {
|
|
6322
|
+
const out = gh(["pr", "view", pr, "--json", "comments"]);
|
|
6323
|
+
const parsed = JSON.parse(out);
|
|
6324
|
+
return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
|
|
6325
|
+
}
|
|
6326
|
+
async function reviewPostCommand(args) {
|
|
6327
|
+
const json = args.includes("--json");
|
|
6328
|
+
const flags2 = parseFlags(args.filter((a) => a !== "--json"));
|
|
6329
|
+
const role = flags2.get("--role");
|
|
6330
|
+
if (role !== "code-reviewer" && role !== "security") {
|
|
6331
|
+
refuseCmd(`\`--role ${role ?? "(missing)"}\` is not \`code-reviewer\` or \`security\`.`, "Pass `--role code-reviewer` or `--role security`.");
|
|
6332
|
+
}
|
|
6333
|
+
const pr = requireFlag(flags2, "--pr");
|
|
6334
|
+
const taskId = requireFlag(flags2, "--task-id");
|
|
6335
|
+
const model = requireFlag(flags2, "--model");
|
|
6336
|
+
const tokensIn = requireTokenField(flags2, "--tokens-in");
|
|
6337
|
+
const tokensOut = requireTokenField(flags2, "--tokens-out");
|
|
6338
|
+
const cost = requireFlag(flags2, "--cost");
|
|
6339
|
+
const tokens = { taskId, model, tokensIn, tokensOut, cost };
|
|
6340
|
+
const headSha = resolveHeadSha(pr);
|
|
6341
|
+
const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
6342
|
+
let body;
|
|
6343
|
+
let verify;
|
|
6344
|
+
if (role === "code-reviewer") {
|
|
6345
|
+
const verdict = normalizeCodeReviewVerdict(requireFlag(flags2, "--verdict"));
|
|
6346
|
+
const findings = readFindingsFile(flags2.get("--findings-file"), CODE_REVIEW_SEVERITIES);
|
|
6347
|
+
if (findings.some((f) => f.severity === "BLOCKER") && verdict === "APPROVE") {
|
|
6348
|
+
refuseCmd("Findings include a BLOCKER but `--verdict APPROVE` — reviewer.md requires REQUEST CHANGES whenever any BLOCKER finding exists.", "Pass `--verdict REQUEST_CHANGES`, or fix the finding severity if BLOCKER was a miscategorization.");
|
|
6349
|
+
}
|
|
6350
|
+
const input = {
|
|
6351
|
+
...tokens,
|
|
6352
|
+
headSha,
|
|
6353
|
+
verdict,
|
|
6354
|
+
briefConformance: requireFlag(flags2, "--brief-conformance"),
|
|
6355
|
+
specConformance: requireFlag(flags2, "--spec-conformance"),
|
|
6356
|
+
findings,
|
|
6357
|
+
scope: requireFlag(flags2, "--scope"),
|
|
6358
|
+
tests: requireFlag(flags2, "--tests"),
|
|
6359
|
+
docs: requireFlag(flags2, "--docs")
|
|
6360
|
+
};
|
|
6361
|
+
body = renderCodeReviewComment(input);
|
|
6362
|
+
verify = (comments2) => verifyPostedCodeReview(comments2, verdict, headSha, principalAllowlist);
|
|
6363
|
+
} else {
|
|
6364
|
+
const verdict = normalizeSecurityVerdict(requireFlag(flags2, "--verdict"));
|
|
6365
|
+
const findings = readFindingsFile(flags2.get("--findings-file"), SECURITY_SEVERITIES);
|
|
6366
|
+
if (findings.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") && verdict === "PASS") {
|
|
6367
|
+
refuseCmd("Findings include a CRITICAL/HIGH finding but `--verdict PASS` — security.md requires FAIL whenever any CRITICAL or HIGH finding exists.", "Pass `--verdict FAIL`, or fix the finding severity if it was a miscategorization.");
|
|
6368
|
+
}
|
|
6369
|
+
const secrets = requireFlag(flags2, "--secrets");
|
|
6370
|
+
const secretsEvidenceFile = flags2.get("--secrets-evidence-file");
|
|
6371
|
+
if (isNoneFoundClaim(secrets) && !secretsEvidenceFile) {
|
|
6372
|
+
refuseCmd('`--secrets` normalizes to "none found" but no `--secrets-evidence-file` was given — security.md: "SECRETS: none found" with no scan output pasted is an unbacked self-attestation.', "Pass `--secrets-evidence-file <path>` containing the actual scanner output, or change `--secrets` to describe what was found instead.");
|
|
6373
|
+
}
|
|
6374
|
+
let secretsEvidence = null;
|
|
6375
|
+
if (secretsEvidenceFile) {
|
|
6376
|
+
try {
|
|
6377
|
+
secretsEvidence = readFileSync17(secretsEvidenceFile, "utf8");
|
|
6378
|
+
} catch {
|
|
6379
|
+
refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
const input = {
|
|
6383
|
+
...tokens,
|
|
6384
|
+
headSha,
|
|
6385
|
+
verdict,
|
|
6386
|
+
findings,
|
|
6387
|
+
configScan: requireFlag(flags2, "--config-scan"),
|
|
6388
|
+
secrets,
|
|
6389
|
+
secretsEvidence
|
|
6390
|
+
};
|
|
6391
|
+
body = renderSecurityComment(input);
|
|
6392
|
+
verify = (comments2) => verifyPostedSecurity(comments2, verdict, headSha, principalAllowlist);
|
|
6393
|
+
}
|
|
6394
|
+
const url = postComment(pr, body);
|
|
6395
|
+
let comments;
|
|
6396
|
+
try {
|
|
6397
|
+
comments = fetchComments(pr);
|
|
6398
|
+
} catch (err) {
|
|
6399
|
+
refuseCmd(`Posted the comment (${url}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
|
|
6400
|
+
}
|
|
6401
|
+
const result = verify(comments);
|
|
6402
|
+
if (!result.ok) {
|
|
6403
|
+
refuseCmd(`Posted comment ${url}, but self-verification FAILED on re-parse: ${result.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
|
|
6404
|
+
}
|
|
6405
|
+
if (json) {
|
|
6406
|
+
printJson({ posted: true, url, role, headSha, selfVerified: true });
|
|
6407
|
+
} else {
|
|
6408
|
+
process.stdout.write(`${body}
|
|
6409
|
+
|
|
6410
|
+
Posted: ${url}
|
|
6411
|
+
Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
|
|
6412
|
+
`);
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
|
|
5520
6416
|
// src/commands/studio.ts
|
|
5521
6417
|
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
5522
|
-
import { existsSync as
|
|
6418
|
+
import { existsSync as existsSync17, readFileSync as readFileSync18, renameSync } from "node:fs";
|
|
5523
6419
|
import net from "node:net";
|
|
5524
|
-
import { dirname as dirname7, join as
|
|
6420
|
+
import { dirname as dirname7, join as join20 } from "node:path";
|
|
5525
6421
|
import { promisify as promisify5 } from "node:util";
|
|
5526
6422
|
|
|
5527
6423
|
// src/lib/studio-bundle.ts
|
|
@@ -5537,25 +6433,25 @@ var FALLBACK_PORT = 3108;
|
|
|
5537
6433
|
function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
5538
6434
|
let dir = cwd;
|
|
5539
6435
|
for (;; ) {
|
|
5540
|
-
const webDir =
|
|
5541
|
-
const pkgPath =
|
|
5542
|
-
if (
|
|
6436
|
+
const webDir = join20(dir, "apps", "vinaya-studio", "web");
|
|
6437
|
+
const pkgPath = join20(webDir, "package.json");
|
|
6438
|
+
if (existsSync17(pkgPath)) {
|
|
5543
6439
|
try {
|
|
5544
|
-
const pkg = JSON.parse(
|
|
6440
|
+
const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
|
|
5545
6441
|
if (pkg.name === "@atta/vinaya-studio-web") {
|
|
5546
6442
|
return { kind: "workspace", webDir };
|
|
5547
6443
|
}
|
|
5548
6444
|
} catch {}
|
|
5549
6445
|
}
|
|
5550
|
-
if (
|
|
6446
|
+
if (existsSync17(join20(dir, ".git")))
|
|
5551
6447
|
break;
|
|
5552
6448
|
const parent = dirname7(dir);
|
|
5553
6449
|
if (parent === dir)
|
|
5554
6450
|
break;
|
|
5555
6451
|
dir = parent;
|
|
5556
6452
|
}
|
|
5557
|
-
const standaloneWebDir =
|
|
5558
|
-
if (
|
|
6453
|
+
const standaloneWebDir = join20(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
|
|
6454
|
+
if (existsSync17(join20(standaloneWebDir, "server.js"))) {
|
|
5559
6455
|
return { kind: "package", packageDir: standaloneWebDir };
|
|
5560
6456
|
}
|
|
5561
6457
|
return { kind: "missing" };
|
|
@@ -5575,9 +6471,9 @@ function isPortFree(port) {
|
|
|
5575
6471
|
});
|
|
5576
6472
|
}
|
|
5577
6473
|
function ensureStudioNodeModules(bundleRoot) {
|
|
5578
|
-
const real =
|
|
5579
|
-
const packed =
|
|
5580
|
-
if (!
|
|
6474
|
+
const real = join20(bundleRoot, "node_modules");
|
|
6475
|
+
const packed = join20(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
|
|
6476
|
+
if (!existsSync17(real) && existsSync17(packed)) {
|
|
5581
6477
|
renameSync(packed, real);
|
|
5582
6478
|
}
|
|
5583
6479
|
}
|
|
@@ -5603,8 +6499,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
5603
6499
|
case "workspace":
|
|
5604
6500
|
return spawnDev(target.webDir, args);
|
|
5605
6501
|
case "package": {
|
|
5606
|
-
const bundleRoot =
|
|
5607
|
-
return spawnStandalone(cwd,
|
|
6502
|
+
const bundleRoot = join20(target.packageDir, "..", "..", "..");
|
|
6503
|
+
return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot);
|
|
5608
6504
|
}
|
|
5609
6505
|
case "missing":
|
|
5610
6506
|
console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya-studio/web), `vinaya studio` runs it directly.");
|
|
@@ -5613,8 +6509,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
5613
6509
|
}
|
|
5614
6510
|
|
|
5615
6511
|
// src/commands/upgrade.ts
|
|
5616
|
-
import { existsSync as
|
|
5617
|
-
import { join as
|
|
6512
|
+
import { existsSync as existsSync18, readFileSync as readFileSync19, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "node:fs";
|
|
6513
|
+
import { join as join21 } from "node:path";
|
|
5618
6514
|
function realDeps7() {
|
|
5619
6515
|
return {
|
|
5620
6516
|
detectRepo: detectGitRepo,
|
|
@@ -5632,12 +6528,12 @@ function flags2(args) {
|
|
|
5632
6528
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
5633
6529
|
}
|
|
5634
6530
|
function readManifest3(repoRoot) {
|
|
5635
|
-
const p =
|
|
5636
|
-
if (!
|
|
6531
|
+
const p = join21(repoRoot, CONFIG_PATH);
|
|
6532
|
+
if (!existsSync18(p))
|
|
5637
6533
|
return { kind: "missing" };
|
|
5638
6534
|
let raw;
|
|
5639
6535
|
try {
|
|
5640
|
-
raw = JSON.parse(
|
|
6536
|
+
raw = JSON.parse(readFileSync19(p, "utf-8"));
|
|
5641
6537
|
} catch (err) {
|
|
5642
6538
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
5643
6539
|
}
|
|
@@ -5653,17 +6549,17 @@ function readManifest3(repoRoot) {
|
|
|
5653
6549
|
return { kind: "ok", manifest: parsed.data.managed };
|
|
5654
6550
|
}
|
|
5655
6551
|
function writeManifestVersion(repoRoot, manifest) {
|
|
5656
|
-
const configAbs =
|
|
5657
|
-
const seed = JSON.parse(
|
|
6552
|
+
const configAbs = join21(repoRoot, CONFIG_PATH);
|
|
6553
|
+
const seed = JSON.parse(readFileSync19(configAbs, "utf-8"));
|
|
5658
6554
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
5659
|
-
|
|
6555
|
+
writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
5660
6556
|
`, "utf-8");
|
|
5661
6557
|
}
|
|
5662
6558
|
function stripFor(repoRoot, path, marker, comment) {
|
|
5663
6559
|
const abs2 = resolveManagedBlockPath(repoRoot, path);
|
|
5664
|
-
if (!
|
|
6560
|
+
if (!existsSync18(abs2))
|
|
5665
6561
|
return { path, marker, comment, present: false, removesHost: false };
|
|
5666
|
-
const stripped = stripBlockFromContent(
|
|
6562
|
+
const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), marker, comment);
|
|
5667
6563
|
if (stripped === null)
|
|
5668
6564
|
return { path, marker, comment, present: false, removesHost: false };
|
|
5669
6565
|
return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
|
|
@@ -5690,9 +6586,9 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
|
|
|
5690
6586
|
const blocked = [];
|
|
5691
6587
|
for (const b of legacy) {
|
|
5692
6588
|
const abs2 = resolveManagedBlockPath(repoRoot, b.path);
|
|
5693
|
-
if (!
|
|
6589
|
+
if (!existsSync18(abs2))
|
|
5694
6590
|
continue;
|
|
5695
|
-
const stripped = stripBlockFromContent(
|
|
6591
|
+
const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), b.marker, b.comment);
|
|
5696
6592
|
if (stripped === null)
|
|
5697
6593
|
blocked.push(`${b.path} exists without vinaya's managed block`);
|
|
5698
6594
|
else if (!blockStripLeavesEmpty(stripped))
|
|
@@ -5727,8 +6623,8 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5727
6623
|
const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
|
|
5728
6624
|
for (const op of ops) {
|
|
5729
6625
|
if (op.kind === "create-file") {
|
|
5730
|
-
const abs2 =
|
|
5731
|
-
const exists =
|
|
6626
|
+
const abs2 = join21(repoRoot, op.path);
|
|
6627
|
+
const exists = existsSync18(abs2);
|
|
5732
6628
|
const owned = ownedFiles.has(op.path);
|
|
5733
6629
|
let action;
|
|
5734
6630
|
if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
|
|
@@ -5738,7 +6634,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5738
6634
|
} else if (!exists) {
|
|
5739
6635
|
action = "recreate";
|
|
5740
6636
|
hasChanges = true;
|
|
5741
|
-
} else if (
|
|
6637
|
+
} else if (readFileSync19(abs2, "utf-8") !== op.content) {
|
|
5742
6638
|
action = "regenerate";
|
|
5743
6639
|
hasChanges = true;
|
|
5744
6640
|
} else {
|
|
@@ -5751,11 +6647,11 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5751
6647
|
let action;
|
|
5752
6648
|
if (!owned) {
|
|
5753
6649
|
action = "not-installed";
|
|
5754
|
-
} else if (!
|
|
6650
|
+
} else if (!existsSync18(abs2)) {
|
|
5755
6651
|
action = "recreate-host";
|
|
5756
6652
|
hasChanges = true;
|
|
5757
6653
|
} else {
|
|
5758
|
-
const content =
|
|
6654
|
+
const content = readFileSync19(abs2, "utf-8");
|
|
5759
6655
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
5760
6656
|
if (!(content.includes(begin) && content.includes(end))) {
|
|
5761
6657
|
action = "recreate-append";
|
|
@@ -5855,10 +6751,10 @@ function renderUpgradeDiff(plan) {
|
|
|
5855
6751
|
}
|
|
5856
6752
|
function regenerateBlock(repoRoot, op) {
|
|
5857
6753
|
const abs2 = resolveManagedBlockPath(repoRoot, op.path);
|
|
5858
|
-
const content =
|
|
6754
|
+
const content = readFileSync19(abs2, "utf-8");
|
|
5859
6755
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
5860
6756
|
if (stripped !== null) {
|
|
5861
|
-
|
|
6757
|
+
writeFileSync11(abs2, stripped.endsWith(`
|
|
5862
6758
|
`) ? stripped : `${stripped}
|
|
5863
6759
|
`, "utf-8");
|
|
5864
6760
|
}
|
|
@@ -5868,7 +6764,7 @@ function applyUpgrade(plan, repoRoot) {
|
|
|
5868
6764
|
for (const e of plan.entries) {
|
|
5869
6765
|
if (e.kind === "create-file") {
|
|
5870
6766
|
if (e.action === "regenerate" || e.action === "recreate") {
|
|
5871
|
-
writeFileWithDirs(
|
|
6767
|
+
writeFileWithDirs(join21(repoRoot, e.op.path), e.op.content, e.op.mode);
|
|
5872
6768
|
}
|
|
5873
6769
|
} else {
|
|
5874
6770
|
if (e.action === "recreate-host")
|
|
@@ -5883,15 +6779,15 @@ function applyUpgrade(plan, repoRoot) {
|
|
|
5883
6779
|
if (!s.present)
|
|
5884
6780
|
continue;
|
|
5885
6781
|
const abs2 = resolveManagedBlockPath(repoRoot, s.path);
|
|
5886
|
-
if (!
|
|
6782
|
+
if (!existsSync18(abs2))
|
|
5887
6783
|
continue;
|
|
5888
|
-
const stripped = stripBlockFromContent(
|
|
6784
|
+
const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), s.marker, s.comment);
|
|
5889
6785
|
if (stripped === null)
|
|
5890
6786
|
continue;
|
|
5891
6787
|
if (blockStripLeavesEmpty(stripped))
|
|
5892
|
-
|
|
6788
|
+
rmSync5(abs2, { force: true });
|
|
5893
6789
|
else
|
|
5894
|
-
|
|
6790
|
+
writeFileSync11(abs2, stripped.endsWith(`
|
|
5895
6791
|
`) ? stripped : `${stripped}
|
|
5896
6792
|
`, "utf-8");
|
|
5897
6793
|
}
|
|
@@ -5973,16 +6869,16 @@ async function upgradeCommand(args) {
|
|
|
5973
6869
|
}
|
|
5974
6870
|
|
|
5975
6871
|
// src/commands/waiver.ts
|
|
5976
|
-
import { execFileSync as
|
|
6872
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
5977
6873
|
function labelFor(kind) {
|
|
5978
6874
|
return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
|
|
5979
6875
|
}
|
|
5980
|
-
function
|
|
5981
|
-
return
|
|
6876
|
+
function gh2(args) {
|
|
6877
|
+
return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
|
|
5982
6878
|
}
|
|
5983
6879
|
function detectPrNumber() {
|
|
5984
6880
|
try {
|
|
5985
|
-
const out =
|
|
6881
|
+
const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
|
|
5986
6882
|
return /^\d+$/.test(out) ? out : null;
|
|
5987
6883
|
} catch {
|
|
5988
6884
|
return null;
|
|
@@ -6078,8 +6974,8 @@ Reason: ${reason}`];
|
|
|
6078
6974
|
}
|
|
6079
6975
|
process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
|
|
6080
6976
|
`);
|
|
6081
|
-
|
|
6082
|
-
|
|
6977
|
+
execFileSync14("gh", editArgs, { stdio: "inherit", env: process.env });
|
|
6978
|
+
execFileSync14("gh", commentArgs, { stdio: "inherit", env: process.env });
|
|
6083
6979
|
process.stdout.write(`
|
|
6084
6980
|
✓ Label applied and reason posted as a PR comment.
|
|
6085
6981
|
`);
|
|
@@ -6149,7 +7045,8 @@ var COMMANDS = [
|
|
|
6149
7045
|
],
|
|
6150
7046
|
details: [
|
|
6151
7047
|
"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.",
|
|
6152
|
-
"
|
|
7048
|
+
"The resolved registry IS what runs. A `checks` key that exactly matches a core check id REPLACES that core check (the core one does not run); any other key must be namespaced `<yourname>/<id>`. Anything the resolver cannot classify — a malformed entry, a bare un-namespaced key matching no core id, a duplicate id — makes `vinaya check` refuse the ENTIRE run: exit 1, nothing executes, never a partial ruleset and never a core-only fallback. `vinaya doctor` carries the permanent diagnostic for each rejected entry, so a refused config is still diagnosable.",
|
|
7049
|
+
"`--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 always renders inline rather than being dropped, and exits non-zero. `--plan` and execution read the same resolution, so what the plan prints is what runs.",
|
|
6153
7050
|
"`--local` exists because a `requiresOpenPr` check (the core `closes-n`/`test-plan`, or a custom check declaring the same field) can only evaluate for real once a pull request exists — the generated `pre-commit`/`pre-push` hooks pass it so the first commit on a fresh task branch is never asked to satisfy a PR-body field before a PR can possibly exist. CI's `vinaya-checks.yml` omits it, so these checks always run for real once a PR is open."
|
|
6154
7051
|
],
|
|
6155
7052
|
status: "shipped"
|
|
@@ -6157,6 +7054,9 @@ var COMMANDS = [
|
|
|
6157
7054
|
{
|
|
6158
7055
|
name: "new check",
|
|
6159
7056
|
description: "Scaffold a custom check into ./scripts/vinaya-checks/",
|
|
7057
|
+
details: [
|
|
7058
|
+
"Takes the REGISTRATION KEY, not a bare name: `vinaya new check <yourname>/<id>` writes `./scripts/vinaya-checks/<id>.ts` and prints the namespaced `checks` entry to paste. It refuses a bare, un-namespaced name — `vinaya check` refuses its entire run over a key it cannot resolve, so scaffolding one would brick every check invocation in the repo — and refuses a core check id, since registering one REPLACES that core gate and a scaffolded stub is never what an adopter means by that."
|
|
7059
|
+
],
|
|
6160
7060
|
status: "shipped"
|
|
6161
7061
|
},
|
|
6162
7062
|
{
|
|
@@ -6191,6 +7091,21 @@ var COMMANDS = [
|
|
|
6191
7091
|
],
|
|
6192
7092
|
status: "shipped"
|
|
6193
7093
|
},
|
|
7094
|
+
{
|
|
7095
|
+
name: "pr report",
|
|
7096
|
+
description: "Emit the AEG:EVIDENCE block — a PR body's factual claims, from commands, never typed",
|
|
7097
|
+
flags: [
|
|
7098
|
+
{
|
|
7099
|
+
flag: "--write",
|
|
7100
|
+
description: "Path to the PR body file; replaces the content between the AEG:EVIDENCE anchors in place"
|
|
7101
|
+
}
|
|
7102
|
+
],
|
|
7103
|
+
details: [
|
|
7104
|
+
"Two groups: Group A (recomputable) is the head sha and a width-invariant `git diff --numstat` against the merge-base with `BASE_SHA` (else `origin/main`, then `main`) — `check-evidence-fresh` recomputes and byte-compares this exactly. Group B (attested) is the result of `vinaya check --all --diff-only`, this CLI's own portable gate suite — `check-evidence-fresh` can only check it for staleness (the block's recorded head still matches the PR's real head), never re-run it.",
|
|
7105
|
+
"With no `--write`, prints the block to stdout instead of writing a file. Exits non-zero whenever the gate run failed, whether or not `--write` was given — the block records a failing result rather than hiding one, and a non-zero exit stops a scripted `--write && open-pr` from carrying a failing suite onto the forge."
|
|
7106
|
+
],
|
|
7107
|
+
status: "shipped"
|
|
7108
|
+
},
|
|
6194
7109
|
{
|
|
6195
7110
|
name: "issue create",
|
|
6196
7111
|
description: "Open an issue after full brief-schema validation",
|
|
@@ -6220,12 +7135,55 @@ var COMMANDS = [
|
|
|
6220
7135
|
],
|
|
6221
7136
|
status: "shipped"
|
|
6222
7137
|
},
|
|
7138
|
+
{
|
|
7139
|
+
name: "review post",
|
|
7140
|
+
description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
|
|
7141
|
+
flags: [
|
|
7142
|
+
{ flag: "--role", description: "`code-reviewer` or `security` — selects which role template is rendered" },
|
|
7143
|
+
{
|
|
7144
|
+
flag: "--pr",
|
|
7145
|
+
description: "Target PR number — its real head is resolved via `gh pr view`, never caller-supplied"
|
|
7146
|
+
},
|
|
7147
|
+
{
|
|
7148
|
+
flag: "--verdict",
|
|
7149
|
+
description: "code-reviewer: `APPROVE` | `REQUEST_CHANGES`. security: `PASS` | `FAIL`"
|
|
7150
|
+
},
|
|
7151
|
+
{
|
|
7152
|
+
flag: "--findings-file",
|
|
7153
|
+
description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). Omit for zero findings."
|
|
7154
|
+
},
|
|
7155
|
+
{ flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
|
|
7156
|
+
{ flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
|
|
7157
|
+
{ flag: "--scope", description: "code-reviewer only: the SCOPE line" },
|
|
7158
|
+
{ flag: "--tests", description: "code-reviewer only: the TESTS line" },
|
|
7159
|
+
{ flag: "--docs", description: "code-reviewer only: the DOCS line" },
|
|
7160
|
+
{ flag: "--config-scan", description: "security only: the CONFIG SCAN line" },
|
|
7161
|
+
{ flag: "--secrets", description: "security only: the SECRETS line" },
|
|
7162
|
+
{
|
|
7163
|
+
flag: "--secrets-evidence-file",
|
|
7164
|
+
description: 'security only: required whenever `--secrets` claims "none found" — the pasted scanner output backing that claim'
|
|
7165
|
+
},
|
|
7166
|
+
{ flag: "--task-id", description: "the closing `Tokens:` line's task id" },
|
|
7167
|
+
{ flag: "--model", description: "the closing `Tokens:` line's model name" },
|
|
7168
|
+
{ flag: "--tokens-in", description: "a non-negative integer, or `-` if unknown" },
|
|
7169
|
+
{ flag: "--tokens-out", description: "a non-negative integer, or `-` if unknown" },
|
|
7170
|
+
{ flag: "--cost", description: "free text, or `-` if unknown" },
|
|
7171
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
7172
|
+
],
|
|
7173
|
+
details: [
|
|
7174
|
+
"Every structural line (`VERDICT:`, `Judged head:`) is rendered from this command's own validated enum/sha inputs — never from a caller-supplied string — so a Reviewer's free-typed prose can no longer produce a shape the merge gate's line-anchored regex fails to see.",
|
|
7175
|
+
"Refuses a contradictory verdict before posting anything: a BLOCKER finding with `--verdict APPROVE`, or a CRITICAL/HIGH finding with `--verdict PASS`, is rejected outright, mirroring each role doc's own consistency rule.",
|
|
7176
|
+
"After posting, re-fetches the PR's comments and runs them through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls — the same functions, not a second implementation — and exits non-zero naming precisely what failed to re-parse if the post does not come back clean and bound to the resolved head. There is no `--skip-verify` escape."
|
|
7177
|
+
],
|
|
7178
|
+
status: "shipped"
|
|
7179
|
+
},
|
|
6223
7180
|
{
|
|
6224
7181
|
name: "doctor",
|
|
6225
7182
|
description: "Diagnose hook, workflow, and config health — report only, never mutates",
|
|
6226
7183
|
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
|
|
6227
7184
|
details: [
|
|
6228
|
-
'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).'
|
|
7185
|
+
'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).',
|
|
7186
|
+
"Also carries the two permanent `checks`-classification diagnostics: a config key that REPLACES a core check (`warn`), and a bare un-namespaced key that is REJECTED (`error`, naming the rename requirement). These are the reason a config `vinaya check` now refuses outright is still diagnosable — the refusal runs nothing, so `doctor` is the surface that explains why."
|
|
6229
7187
|
],
|
|
6230
7188
|
status: "shipped"
|
|
6231
7189
|
},
|
|
@@ -6364,9 +7322,9 @@ function printHelp() {
|
|
|
6364
7322
|
}
|
|
6365
7323
|
|
|
6366
7324
|
// src/index.ts
|
|
6367
|
-
var PACKAGE_ROOT =
|
|
7325
|
+
var PACKAGE_ROOT = join22(dirname8(fileURLToPath2(import.meta.url)), "..");
|
|
6368
7326
|
function readVersion2() {
|
|
6369
|
-
const pkg = JSON.parse(
|
|
7327
|
+
const pkg = JSON.parse(readFileSync20(join22(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
6370
7328
|
return pkg.version;
|
|
6371
7329
|
}
|
|
6372
7330
|
var [, , command, ...args] = process.argv;
|
|
@@ -6449,8 +7407,10 @@ try {
|
|
|
6449
7407
|
prCreateCommand(rest);
|
|
6450
7408
|
} else if (subcommand === "edit") {
|
|
6451
7409
|
prEditCommand(rest);
|
|
7410
|
+
} else if (subcommand === "report") {
|
|
7411
|
+
await prReportCommand(rest);
|
|
6452
7412
|
} else {
|
|
6453
|
-
console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create' or '
|
|
7413
|
+
console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', or 'report')`);
|
|
6454
7414
|
process.exit(2);
|
|
6455
7415
|
}
|
|
6456
7416
|
break;
|
|
@@ -6467,6 +7427,16 @@ try {
|
|
|
6467
7427
|
}
|
|
6468
7428
|
break;
|
|
6469
7429
|
}
|
|
7430
|
+
case "review": {
|
|
7431
|
+
const [subcommand, ...rest] = args;
|
|
7432
|
+
if (subcommand === "post") {
|
|
7433
|
+
await reviewPostCommand(rest);
|
|
7434
|
+
} else {
|
|
7435
|
+
console.error(`Unknown 'review' subcommand: ${subcommand ?? "(none)"} (expected 'post')`);
|
|
7436
|
+
process.exit(2);
|
|
7437
|
+
}
|
|
7438
|
+
break;
|
|
7439
|
+
}
|
|
6470
7440
|
case "demo": {
|
|
6471
7441
|
const [subcommand, ...rest] = args;
|
|
6472
7442
|
if (subcommand === "break") {
|