@attalabs/vinaya 0.12.0 → 0.17.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 -3
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +5 -5
- package/aeg-root/roles/developer.md +1 -0
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/reviewer.md +17 -4
- package/aeg-root/roles/security.md +16 -5
- package/aeg-root/skills/aeg/SKILL.md +1 -1
- package/aeg-root/state-machine.md +2 -2
- package/aeg-root/templates/pr-report-template.md +2 -0
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-body-bare-digits.js +3545 -0
- package/dist/checks/bin/check-branch-topology.js +99 -10
- package/dist/checks/bin/check-brief-shape.js +99 -10
- package/dist/checks/bin/check-closes-n.js +99 -10
- package/dist/checks/bin/check-coherence.js +106 -12
- package/dist/checks/bin/check-dead-branch-push.js +99 -10
- package/dist/checks/bin/check-dispatch-readiness.js +106 -12
- package/dist/checks/bin/check-doc-coverage-push.js +106 -12
- package/dist/checks/bin/check-doc-coverage.js +109 -16
- package/dist/checks/bin/check-evidence-fresh.js +99 -10
- package/dist/checks/bin/check-first-push-dispatch.js +106 -12
- package/dist/checks/bin/check-issue-assignment.js +99 -10
- package/dist/checks/bin/check-no-disk-state.js +99 -10
- package/dist/checks/bin/check-reader-resolvable-prose.js +99 -10
- package/dist/checks/bin/check-registry-gates.js +99 -10
- package/dist/checks/bin/check-review-gate.js +109 -25
- package/dist/checks/bin/check-single-plan-pr.js +99 -10
- package/dist/checks/bin/check-test-plan.js +99 -10
- package/dist/index.js +1325 -236
- 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 +1 -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 +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- 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]__0gw1_g2._.js +1 -1
- 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 +1 -1
- 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]__1wsl6ro._.js → [root-of-the-server]__1ccgndq._.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]__1dfzjrd._.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/_0-n77dx._.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/_01605g7._.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/{_1u3abhy._.js → _0x999pb._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0ndy563._.js → _1-6rzip._.js} +2 -2
- 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/{_087xa25._.js → _1_talrs._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_11vnfnm._.js → _1ar5uqe._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_02drwa_._.js → _1c9b9j-._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_18kqbvs._.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/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_basic_components_index_ts_0xk1vda._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1mexqnauhtbtp.js → 0_jnwbarv-cqk.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2ieu4oi-pp_bz.js → 0l_vubs1ephe5.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0ojfbn3yvy-8_.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1jx5_q1g9l-cm.js → 0phqfqwbnt_ey.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0qcu17x3h94a9.js → 17r0szvqeeyhx.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ve1-nz-mu_lr.js → 18x3rbqzeutpz.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{22y4d5yrovcv-.js → 1bqpfvromtwn1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{39w6jbw3rleez.js → 24ojyzmt_fkg3.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0tmcnl-z8yt2_.js → 30lyvn0yzwdiz.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 3m0_gk59s-rbx.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1eo32v1tbjy7c.js → 40x7f5hm7a5tk.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0-08g_jjalnd9.js → 453zhecht_m1q.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1t7yy_s34022p.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → p5sWGp4ANwORiaX4Kk6U2}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → p5sWGp4ANwORiaX4Kk6U2}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → p5sWGp4ANwORiaX4Kk6U2}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,3545 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/checks/bin/check-body-bare-digits.ts
|
|
5
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
6
|
+
// ../../packages/aeg-forge-state/src/strip-code.ts
|
|
7
|
+
function maskCode(body) {
|
|
8
|
+
const fill = (line) => " ".repeat(line.length);
|
|
9
|
+
const blocksMasked = maskIndentedCode(maskFencedCode(body, fill), fill);
|
|
10
|
+
return blocksMasked.split(`
|
|
11
|
+
`).map((line) => replaceInlineSpans(line, (span) => " ".repeat(span.length))).join(`
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
var DETAILS_OPEN = /<details\b[^>]*>/gi;
|
|
15
|
+
var DETAILS_CLOSE = /<\/details\s*>/gi;
|
|
16
|
+
function maskDetailsBlocks(body) {
|
|
17
|
+
const fill = (line) => " ".repeat(line.length);
|
|
18
|
+
const lines = body.split(`
|
|
19
|
+
`);
|
|
20
|
+
let depth = 0;
|
|
21
|
+
const out = lines.map((line) => {
|
|
22
|
+
const opens = line.match(DETAILS_OPEN)?.length ?? 0;
|
|
23
|
+
const closes = line.match(DETAILS_CLOSE)?.length ?? 0;
|
|
24
|
+
const maskThisLine = depth > 0 || opens > 0;
|
|
25
|
+
depth = Math.max(0, depth + opens - closes);
|
|
26
|
+
return maskThisLine ? fill(line) : line;
|
|
27
|
+
});
|
|
28
|
+
return out.join(`
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
function stripCode(body, options = {}) {
|
|
32
|
+
const normalised = body.replace(/\r\n?/g, `
|
|
33
|
+
`);
|
|
34
|
+
const blank = () => "";
|
|
35
|
+
const blocksStripped = maskIndentedCode(maskFencedCode(normalised, blank), blank);
|
|
36
|
+
if (options.inlineSpans === "keep")
|
|
37
|
+
return blocksStripped;
|
|
38
|
+
return blocksStripped.split(`
|
|
39
|
+
`).map((line) => replaceInlineSpans(line, () => "")).join(`
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
function replaceInlineSpans(line, replace) {
|
|
43
|
+
let result = "";
|
|
44
|
+
let i = 0;
|
|
45
|
+
const runLengthAt = (at) => {
|
|
46
|
+
let j = at;
|
|
47
|
+
while (j < line.length && line[j] === "`")
|
|
48
|
+
j++;
|
|
49
|
+
return j - at;
|
|
50
|
+
};
|
|
51
|
+
while (i < line.length) {
|
|
52
|
+
if (line[i] !== "`") {
|
|
53
|
+
result += line[i];
|
|
54
|
+
i++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const openLen = runLengthAt(i);
|
|
58
|
+
const openEnd = i + openLen;
|
|
59
|
+
let j = openEnd;
|
|
60
|
+
let closeEnd = -1;
|
|
61
|
+
while (j < line.length) {
|
|
62
|
+
if (line[j] !== "`") {
|
|
63
|
+
j++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const runLen = runLengthAt(j);
|
|
67
|
+
if (runLen === openLen) {
|
|
68
|
+
closeEnd = j + runLen;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
j += runLen;
|
|
72
|
+
}
|
|
73
|
+
if (closeEnd === -1) {
|
|
74
|
+
result += line.slice(i, openEnd);
|
|
75
|
+
i = openEnd;
|
|
76
|
+
} else {
|
|
77
|
+
result += replace(line.slice(i, closeEnd));
|
|
78
|
+
i = closeEnd;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
|
|
84
|
+
var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
|
|
85
|
+
function maskFencedCode(body, fill) {
|
|
86
|
+
const { lines } = scanFencedCode(body);
|
|
87
|
+
return lines.map(({ line, inCode }) => inCode ? fill(line) : line).join(`
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
function scanFencedCode(body) {
|
|
91
|
+
let fenceChar = null;
|
|
92
|
+
let fenceLen = 0;
|
|
93
|
+
const lines = body.split(`
|
|
94
|
+
`).map((line) => {
|
|
95
|
+
if (fenceChar === null) {
|
|
96
|
+
const open = line.match(FENCE_OPEN);
|
|
97
|
+
if (!open)
|
|
98
|
+
return { line, inCode: false };
|
|
99
|
+
const marker = open[1];
|
|
100
|
+
if (marker[0] === "`" && open[2].includes("`"))
|
|
101
|
+
return { line, inCode: false };
|
|
102
|
+
fenceChar = marker[0];
|
|
103
|
+
fenceLen = marker.length;
|
|
104
|
+
return { line, inCode: true };
|
|
105
|
+
}
|
|
106
|
+
const close = line.match(FENCE_CLOSE);
|
|
107
|
+
if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
|
|
108
|
+
fenceChar = null;
|
|
109
|
+
fenceLen = 0;
|
|
110
|
+
}
|
|
111
|
+
return { line, inCode: true };
|
|
112
|
+
});
|
|
113
|
+
return { lines, unterminated: fenceChar !== null };
|
|
114
|
+
}
|
|
115
|
+
function hasUnterminatedFence(body) {
|
|
116
|
+
return scanFencedCode(body.replace(/\r\n?/g, `
|
|
117
|
+
`)).unterminated;
|
|
118
|
+
}
|
|
119
|
+
var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
|
|
120
|
+
function indentWidth(line) {
|
|
121
|
+
let width = 0;
|
|
122
|
+
for (const ch of line) {
|
|
123
|
+
if (ch === " ")
|
|
124
|
+
width += 1;
|
|
125
|
+
else if (ch === "\t")
|
|
126
|
+
width += 4;
|
|
127
|
+
else
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
return width;
|
|
131
|
+
}
|
|
132
|
+
function maskIndentedCode(body, fill) {
|
|
133
|
+
const lines = body.split(`
|
|
134
|
+
`);
|
|
135
|
+
let inList = false;
|
|
136
|
+
let inCode = false;
|
|
137
|
+
let prevBlank = true;
|
|
138
|
+
const out = lines.map((line) => {
|
|
139
|
+
if (line.trim() === "") {
|
|
140
|
+
prevBlank = true;
|
|
141
|
+
return line;
|
|
142
|
+
}
|
|
143
|
+
const indent = indentWidth(line);
|
|
144
|
+
if (indent >= 4 && (inCode || prevBlank && !inList)) {
|
|
145
|
+
inCode = true;
|
|
146
|
+
prevBlank = false;
|
|
147
|
+
return fill(line);
|
|
148
|
+
}
|
|
149
|
+
inCode = false;
|
|
150
|
+
if (indent < 4 && LIST_MARKER.test(line))
|
|
151
|
+
inList = true;
|
|
152
|
+
else if (indent === 0)
|
|
153
|
+
inList = false;
|
|
154
|
+
prevBlank = false;
|
|
155
|
+
return line;
|
|
156
|
+
});
|
|
157
|
+
return out.join(`
|
|
158
|
+
`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ../../packages/aeg-forge-state/src/parse-rationale-deps.ts
|
|
162
|
+
var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
|
|
163
|
+
var NEXT_HEADER = /\*\*[A-Z][^*]*\*\*/;
|
|
164
|
+
var FIELD_LABEL = /^(Depends-on|Conflicts-with)\s*:\s*(.*)$/i;
|
|
165
|
+
var ID_TOKEN = /^(?:[\w.-]+\s+)?#?\d+[a-z]?$/i;
|
|
166
|
+
var SLUG_QUALIFIED_ID = /^([\w.-]+)\s+(#?\d+[a-z]?)$/i;
|
|
167
|
+
function isEmptyMarker(s) {
|
|
168
|
+
const t = s.trim();
|
|
169
|
+
return t === "" || t === "—" || t === "-" || t === "–";
|
|
170
|
+
}
|
|
171
|
+
function pushUnique(arr, ids) {
|
|
172
|
+
for (const id of ids) {
|
|
173
|
+
if (!arr.includes(id))
|
|
174
|
+
arr.push(id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function splitIds(raw) {
|
|
178
|
+
if (isEmptyMarker(raw))
|
|
179
|
+
return [];
|
|
180
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !isEmptyMarker(s) && ID_TOKEN.test(s));
|
|
181
|
+
}
|
|
182
|
+
function resolveIds(raw, lastSlug) {
|
|
183
|
+
const ids = splitIds(raw);
|
|
184
|
+
const resolved = [];
|
|
185
|
+
for (const id of ids) {
|
|
186
|
+
const qualified = id.match(SLUG_QUALIFIED_ID);
|
|
187
|
+
if (qualified) {
|
|
188
|
+
lastSlug.current = qualified[1] ?? null;
|
|
189
|
+
resolved.push(id);
|
|
190
|
+
} else if (lastSlug.current) {
|
|
191
|
+
resolved.push(`${lastSlug.current} ${id}`);
|
|
192
|
+
lastSlug.current = null;
|
|
193
|
+
} else {
|
|
194
|
+
resolved.push(id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return resolved;
|
|
198
|
+
}
|
|
199
|
+
function extractSection(body) {
|
|
200
|
+
const start = body.match(SECTION_HEADER);
|
|
201
|
+
if (!start || start.index === undefined)
|
|
202
|
+
return "";
|
|
203
|
+
const rest = body.slice(start.index + start[0].length);
|
|
204
|
+
const next = rest.match(NEXT_HEADER);
|
|
205
|
+
const end = next && next.index !== undefined ? next.index : rest.length;
|
|
206
|
+
return rest.slice(0, end);
|
|
207
|
+
}
|
|
208
|
+
function parseRationaleDeps(body) {
|
|
209
|
+
const section = extractSection(body);
|
|
210
|
+
const dependsOn = [];
|
|
211
|
+
const conflictsWith = [];
|
|
212
|
+
let current = null;
|
|
213
|
+
const assignedFields = new Set;
|
|
214
|
+
const lastSlugByField = {
|
|
215
|
+
dependsOn: { current: null },
|
|
216
|
+
conflictsWith: { current: null }
|
|
217
|
+
};
|
|
218
|
+
const spanPattern = /`([^`]*)`/g;
|
|
219
|
+
let match = spanPattern.exec(section);
|
|
220
|
+
while (match !== null) {
|
|
221
|
+
const content = (match[1] ?? "").trim();
|
|
222
|
+
const labelMatch = content.match(FIELD_LABEL);
|
|
223
|
+
if (labelMatch) {
|
|
224
|
+
const field = labelMatch[1]?.toLowerCase() === "conflicts-with" ? "conflictsWith" : "dependsOn";
|
|
225
|
+
if (!assignedFields.has(field)) {
|
|
226
|
+
assignedFields.add(field);
|
|
227
|
+
current = field;
|
|
228
|
+
const ids = resolveIds(labelMatch[2] ?? "", lastSlugByField[field]);
|
|
229
|
+
pushUnique(field === "dependsOn" ? dependsOn : conflictsWith, ids);
|
|
230
|
+
}
|
|
231
|
+
} else if (current) {
|
|
232
|
+
const ids = resolveIds(content, lastSlugByField[current]);
|
|
233
|
+
pushUnique(current === "dependsOn" ? dependsOn : conflictsWith, ids);
|
|
234
|
+
}
|
|
235
|
+
match = spanPattern.exec(section);
|
|
236
|
+
}
|
|
237
|
+
return { dependsOn, conflictsWith };
|
|
238
|
+
}
|
|
239
|
+
// ../../packages/aeg-forge-state/src/gh.ts
|
|
240
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
241
|
+
import { promisify } from "node:util";
|
|
242
|
+
var execFileAsync = promisify(execFile);
|
|
243
|
+
var systemEnv = {
|
|
244
|
+
...process.env,
|
|
245
|
+
PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
|
|
246
|
+
};
|
|
247
|
+
function run(args) {
|
|
248
|
+
return execFileSync("gh", args, { encoding: "utf8", env: systemEnv });
|
|
249
|
+
}
|
|
250
|
+
async function runAsync(args) {
|
|
251
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
252
|
+
encoding: "utf8",
|
|
253
|
+
env: systemEnv,
|
|
254
|
+
maxBuffer: 16 * 1024 * 1024
|
|
255
|
+
});
|
|
256
|
+
return stdout;
|
|
257
|
+
}
|
|
258
|
+
function ghApiGet(path) {
|
|
259
|
+
return JSON.parse(run(["api", path]));
|
|
260
|
+
}
|
|
261
|
+
function issueListByLabelArgs(owner, repo, label) {
|
|
262
|
+
return [
|
|
263
|
+
"issue",
|
|
264
|
+
"list",
|
|
265
|
+
"--repo",
|
|
266
|
+
`${owner}/${repo}`,
|
|
267
|
+
"--label",
|
|
268
|
+
label,
|
|
269
|
+
"--state",
|
|
270
|
+
"all",
|
|
271
|
+
"--json",
|
|
272
|
+
"number,title,body,state,labels,milestone",
|
|
273
|
+
"--limit",
|
|
274
|
+
"200"
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
async function ghIssueListByLabelAsync(owner, repo, label) {
|
|
278
|
+
return JSON.parse(await runAsync(issueListByLabelArgs(owner, repo, label)));
|
|
279
|
+
}
|
|
280
|
+
function dedupeByNumber(issues) {
|
|
281
|
+
const byNumber = new Map;
|
|
282
|
+
for (const issue of issues)
|
|
283
|
+
if (!byNumber.has(issue.number))
|
|
284
|
+
byNumber.set(issue.number, issue);
|
|
285
|
+
return [...byNumber.values()];
|
|
286
|
+
}
|
|
287
|
+
async function ghIssueListByAnyLabelAsync(owner, repo, labels) {
|
|
288
|
+
const perLabel = await Promise.all(labels.map((l) => ghIssueListByLabelAsync(owner, repo, l)));
|
|
289
|
+
return dedupeByNumber(perLabel.flat());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ../../packages/aeg-forge-state/src/fetch-milestone.ts
|
|
293
|
+
function findMilestoneForSlug(owner, repo, slug) {
|
|
294
|
+
const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
|
|
295
|
+
const match = milestones.find((m) => m.title === slug);
|
|
296
|
+
if (!match)
|
|
297
|
+
return null;
|
|
298
|
+
return {
|
|
299
|
+
goal: match.description ?? "",
|
|
300
|
+
lifecycle: match.state === "closed" ? "complete" : "active"
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ../../packages/aeg-forge-state/src/labels.ts
|
|
305
|
+
var LABELS = [
|
|
306
|
+
{
|
|
307
|
+
key: "blocked",
|
|
308
|
+
id: "vinaya/blocked",
|
|
309
|
+
category: "state",
|
|
310
|
+
form: "literal",
|
|
311
|
+
carries: "Execution is halted pending an external unblock; wins over every other derived status."
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
key: "tier-0",
|
|
315
|
+
id: "vinaya/tier:0",
|
|
316
|
+
category: "tier",
|
|
317
|
+
form: "literal",
|
|
318
|
+
carries: "Lowest governance weight — typecheck, lint, tests, and a conforming PR body."
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
key: "tier-1",
|
|
322
|
+
id: "vinaya/tier:1",
|
|
323
|
+
category: "tier",
|
|
324
|
+
form: "literal",
|
|
325
|
+
carries: "Tier 0 plus spec/skill coverage and a passing verify-docs run."
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
key: "tier-3",
|
|
329
|
+
id: "vinaya/tier:3",
|
|
330
|
+
category: "tier",
|
|
331
|
+
form: "literal",
|
|
332
|
+
carries: "Tier 1 plus the reasoning for the change, stated in the pull request that makes it."
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
key: "tranche",
|
|
336
|
+
id: "vinaya/tranche:",
|
|
337
|
+
category: "tranche",
|
|
338
|
+
form: "prefix",
|
|
339
|
+
carries: "The tranche slug this task Issue belongs to — the forge's grouping key, matched by prefix."
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
key: "needs-execution-input",
|
|
343
|
+
id: "vinaya/needs:execution-input",
|
|
344
|
+
category: "needs",
|
|
345
|
+
form: "literal",
|
|
346
|
+
carries: "Waiting on a missing execution detail — a flag, a dependency, a value the brief did not carry."
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
key: "needs-strategy-input",
|
|
350
|
+
id: "vinaya/needs:strategy-input",
|
|
351
|
+
category: "needs",
|
|
352
|
+
form: "literal",
|
|
353
|
+
carries: "Waiting on a strategy call — the brief assumes an approach the codebase has moved away from."
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
key: "needs-principal-input",
|
|
357
|
+
id: "vinaya/needs:principal-input",
|
|
358
|
+
category: "needs",
|
|
359
|
+
form: "literal",
|
|
360
|
+
carries: "Waiting on the Principal — a product-level call no agent may make."
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
key: "needs-brief-correction",
|
|
364
|
+
id: "vinaya/needs:brief-correction",
|
|
365
|
+
category: "needs",
|
|
366
|
+
form: "literal",
|
|
367
|
+
carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
key: "waiver-docs",
|
|
371
|
+
id: "vinaya/waiver:docs",
|
|
372
|
+
category: "waiver",
|
|
373
|
+
form: "literal",
|
|
374
|
+
carries: "Doc-coverage gate excused for this PR — honored only when a principal applied it."
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
key: "waiver-review",
|
|
378
|
+
id: "vinaya/waiver:review",
|
|
379
|
+
category: "waiver",
|
|
380
|
+
form: "literal",
|
|
381
|
+
carries: "Review gate excused for this PR — honored only when a principal applied it."
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
key: "override-docs",
|
|
385
|
+
id: "vinaya/override:docs",
|
|
386
|
+
category: "waiver",
|
|
387
|
+
form: "literal",
|
|
388
|
+
carries: "The whole verify-docs gate suppressed for this PR — a Principal-only blunt override, wider than a waiver."
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
key: "incoherent",
|
|
392
|
+
id: "vinaya/incoherent",
|
|
393
|
+
category: "flag",
|
|
394
|
+
form: "literal",
|
|
395
|
+
carries: "Closed COMPLETED with no merged-PR link — done-but-unprovable, surfaced for a human."
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
key: "direct-main-push",
|
|
399
|
+
id: "vinaya/direct-main-push",
|
|
400
|
+
category: "flag",
|
|
401
|
+
form: "literal",
|
|
402
|
+
carries: "A commit reached main with no associated merged PR — ring-2 detection, never a mutation."
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
key: "dead-branch-push",
|
|
406
|
+
id: "vinaya/dead-branch-push",
|
|
407
|
+
category: "flag",
|
|
408
|
+
form: "literal",
|
|
409
|
+
carries: "Commits landed on a branch after its PR had already resolved — daily-drift detection."
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
key: "state-object",
|
|
413
|
+
id: "vinaya/state-object",
|
|
414
|
+
category: "kind",
|
|
415
|
+
form: "literal",
|
|
416
|
+
carries: "A permanent forge-native storage object, never actionable work — excluded from every backlog."
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
var BY_KEY = new Map(LABELS.map((l) => [l.key, l]));
|
|
420
|
+
function entry(key) {
|
|
421
|
+
const found = BY_KEY.get(key);
|
|
422
|
+
if (!found)
|
|
423
|
+
throw new Error(`labels.ts: no LABELS entry for key '${key}'`);
|
|
424
|
+
return found;
|
|
425
|
+
}
|
|
426
|
+
function label(key) {
|
|
427
|
+
return entry(key).id;
|
|
428
|
+
}
|
|
429
|
+
function trancheLabel(slug) {
|
|
430
|
+
return `${label("tranche")}${slug}`;
|
|
431
|
+
}
|
|
432
|
+
function matchesLabel(key, name) {
|
|
433
|
+
const l = entry(key);
|
|
434
|
+
if (l.form === "prefix")
|
|
435
|
+
return name.startsWith(l.id);
|
|
436
|
+
return name === l.id;
|
|
437
|
+
}
|
|
438
|
+
function hasLabel(key, names) {
|
|
439
|
+
return names.some((n) => matchesLabel(key, n));
|
|
440
|
+
}
|
|
441
|
+
function trancheSlugOf(name) {
|
|
442
|
+
const prefix = entry("tranche").id;
|
|
443
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : null;
|
|
444
|
+
}
|
|
445
|
+
function findTrancheSlug(names) {
|
|
446
|
+
for (const n of names) {
|
|
447
|
+
const slug = trancheSlugOf(n);
|
|
448
|
+
if (slug !== null)
|
|
449
|
+
return slug;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ../../packages/aeg-forge-state/src/list-tasks.ts
|
|
455
|
+
var TITLE_PATTERN = /^\[([^\]]+)]\s*(\S+)\s*—\s*(.+)$/;
|
|
456
|
+
var PROJECT_SLUG = /^[a-z0-9][a-z0-9-]*$/i;
|
|
457
|
+
var PROJECT_FIELD = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
|
|
458
|
+
var VALUE_EDGE_MARKUP = /^[`.;\s]+|[`.;\s]+$/g;
|
|
459
|
+
function unwrapValue(raw) {
|
|
460
|
+
return raw.replace(/\*\*/g, "").replace(VALUE_EDGE_MARKUP, "");
|
|
461
|
+
}
|
|
462
|
+
function projectFieldFromBody(body) {
|
|
463
|
+
const m = stripCode(body, { inlineSpans: "keep" }).match(PROJECT_FIELD);
|
|
464
|
+
if (m)
|
|
465
|
+
return parseFieldValue(m[1] ?? "");
|
|
466
|
+
if (hasUnterminatedFence(body)) {
|
|
467
|
+
const raw = body.match(PROJECT_FIELD);
|
|
468
|
+
if (raw)
|
|
469
|
+
return { declared: true, names: [], unparsed: [(raw[1] ?? "").trim()], unreadable: true };
|
|
470
|
+
}
|
|
471
|
+
return { declared: false, names: [], unparsed: [], unreadable: false };
|
|
472
|
+
}
|
|
473
|
+
function parseFieldValue(value) {
|
|
474
|
+
const names = [];
|
|
475
|
+
const unparsed = [];
|
|
476
|
+
for (const raw of value.split(",")) {
|
|
477
|
+
const name = unwrapValue(raw);
|
|
478
|
+
if (name.length === 0)
|
|
479
|
+
continue;
|
|
480
|
+
if (PROJECT_SLUG.test(name))
|
|
481
|
+
names.push(name);
|
|
482
|
+
else
|
|
483
|
+
unparsed.push(raw.trim());
|
|
484
|
+
}
|
|
485
|
+
return { declared: true, names: [...new Set(names)], unparsed, unreadable: false };
|
|
486
|
+
}
|
|
487
|
+
function projectsFromBody(body) {
|
|
488
|
+
return projectFieldFromBody(body).names;
|
|
489
|
+
}
|
|
490
|
+
function taskFromIssue(issue) {
|
|
491
|
+
const m = issue.title.match(TITLE_PATTERN);
|
|
492
|
+
if (!m)
|
|
493
|
+
return null;
|
|
494
|
+
const id = (m[2] ?? "").trim();
|
|
495
|
+
const title = (m[3] ?? "").trim();
|
|
496
|
+
if (!id || !title)
|
|
497
|
+
return null;
|
|
498
|
+
const body = issue.body ?? "";
|
|
499
|
+
const { dependsOn, conflictsWith } = parseRationaleDeps(body);
|
|
500
|
+
return {
|
|
501
|
+
id,
|
|
502
|
+
title,
|
|
503
|
+
issue: issue.number,
|
|
504
|
+
projects: projectsFromBody(body),
|
|
505
|
+
dependsOn,
|
|
506
|
+
conflictsWith,
|
|
507
|
+
rationaleMarkdown: body
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function compareTaskIds(a, b) {
|
|
511
|
+
const numA = a.match(/^(\d+)(.*)$/);
|
|
512
|
+
const numB = b.match(/^(\d+)(.*)$/);
|
|
513
|
+
if (numA && numB) {
|
|
514
|
+
const diff = Number(numA[1]) - Number(numB[1]);
|
|
515
|
+
if (diff !== 0)
|
|
516
|
+
return diff;
|
|
517
|
+
return (numA[2] ?? "").localeCompare(numB[2] ?? "");
|
|
518
|
+
}
|
|
519
|
+
return a.localeCompare(b);
|
|
520
|
+
}
|
|
521
|
+
function tasksFromIssues(issues) {
|
|
522
|
+
const tasks = [];
|
|
523
|
+
for (const issue of issues) {
|
|
524
|
+
const task = taskFromIssue(issue);
|
|
525
|
+
if (task)
|
|
526
|
+
tasks.push(task);
|
|
527
|
+
}
|
|
528
|
+
return tasks.sort((a, b) => compareTaskIds(a.id, b.id));
|
|
529
|
+
}
|
|
530
|
+
async function fetchTrancheIssuesAsync(owner, repo, slug) {
|
|
531
|
+
return ghIssueListByAnyLabelAsync(owner, repo, [trancheLabel(slug)]);
|
|
532
|
+
}
|
|
533
|
+
function resolveTaskIssueRef(title, labels) {
|
|
534
|
+
const m = title.match(TITLE_PATTERN);
|
|
535
|
+
if (!m)
|
|
536
|
+
return null;
|
|
537
|
+
const taskId = (m[2] ?? "").trim();
|
|
538
|
+
if (!taskId)
|
|
539
|
+
return null;
|
|
540
|
+
const trancheSlug = findTrancheSlug(labels);
|
|
541
|
+
if (!trancheSlug)
|
|
542
|
+
return null;
|
|
543
|
+
return { trancheSlug, taskId };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ../../packages/aeg-forge-state/src/derive-from-forge.ts
|
|
547
|
+
function trancheFromIssues(slug, issues, known) {
|
|
548
|
+
return {
|
|
549
|
+
name: slug,
|
|
550
|
+
lifecycle: known?.lifecycle ?? "active",
|
|
551
|
+
goal: known?.goal ?? "",
|
|
552
|
+
tasks: tasksFromIssues(issues),
|
|
553
|
+
backlog: []
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async function deriveTrancheFromForge(owner, repo, slug, known) {
|
|
557
|
+
const milestone = known ?? findMilestoneForSlug(owner, repo, slug);
|
|
558
|
+
const issues = await fetchTrancheIssuesAsync(owner, repo, slug);
|
|
559
|
+
return trancheFromIssues(slug, issues, milestone);
|
|
560
|
+
}
|
|
561
|
+
// ../../node_modules/universal-user-agent/index.js
|
|
562
|
+
function getUserAgent() {
|
|
563
|
+
if (typeof navigator === "object" && "userAgent" in navigator) {
|
|
564
|
+
return navigator.userAgent;
|
|
565
|
+
}
|
|
566
|
+
if (typeof process === "object" && process.version !== undefined) {
|
|
567
|
+
return `Node.js/${process.version.substr(1)} (${process.platform}; ${process.arch})`;
|
|
568
|
+
}
|
|
569
|
+
return "<environment undetectable>";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ../../node_modules/@octokit/endpoint/dist-bundle/index.js
|
|
573
|
+
var VERSION = "0.0.0-development";
|
|
574
|
+
var userAgent = `octokit-endpoint.js/${VERSION} ${getUserAgent()}`;
|
|
575
|
+
var DEFAULTS = {
|
|
576
|
+
method: "GET",
|
|
577
|
+
baseUrl: "https://api.github.com",
|
|
578
|
+
headers: {
|
|
579
|
+
accept: "application/vnd.github.v3+json",
|
|
580
|
+
"user-agent": userAgent
|
|
581
|
+
},
|
|
582
|
+
mediaType: {
|
|
583
|
+
format: ""
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
function lowercaseKeys(object) {
|
|
587
|
+
if (!object) {
|
|
588
|
+
return {};
|
|
589
|
+
}
|
|
590
|
+
return Object.keys(object).reduce((newObj, key) => {
|
|
591
|
+
newObj[key.toLowerCase()] = object[key];
|
|
592
|
+
return newObj;
|
|
593
|
+
}, {});
|
|
594
|
+
}
|
|
595
|
+
function isPlainObject(value) {
|
|
596
|
+
if (typeof value !== "object" || value === null)
|
|
597
|
+
return false;
|
|
598
|
+
if (Object.prototype.toString.call(value) !== "[object Object]")
|
|
599
|
+
return false;
|
|
600
|
+
const proto = Object.getPrototypeOf(value);
|
|
601
|
+
if (proto === null)
|
|
602
|
+
return true;
|
|
603
|
+
const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
|
|
604
|
+
return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
|
|
605
|
+
}
|
|
606
|
+
function mergeDeep(defaults, options) {
|
|
607
|
+
const result = Object.assign({}, defaults);
|
|
608
|
+
Object.keys(options).forEach((key) => {
|
|
609
|
+
if (isPlainObject(options[key])) {
|
|
610
|
+
if (!(key in defaults))
|
|
611
|
+
Object.assign(result, { [key]: options[key] });
|
|
612
|
+
else
|
|
613
|
+
result[key] = mergeDeep(defaults[key], options[key]);
|
|
614
|
+
} else {
|
|
615
|
+
Object.assign(result, { [key]: options[key] });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
function removeUndefinedProperties(obj) {
|
|
621
|
+
for (const key in obj) {
|
|
622
|
+
if (obj[key] === undefined) {
|
|
623
|
+
delete obj[key];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return obj;
|
|
627
|
+
}
|
|
628
|
+
function merge(defaults, route, options) {
|
|
629
|
+
if (typeof route === "string") {
|
|
630
|
+
let [method, url] = route.split(" ");
|
|
631
|
+
options = Object.assign(url ? { method, url } : { url: method }, options);
|
|
632
|
+
} else {
|
|
633
|
+
options = Object.assign({}, route);
|
|
634
|
+
}
|
|
635
|
+
options.headers = lowercaseKeys(options.headers);
|
|
636
|
+
removeUndefinedProperties(options);
|
|
637
|
+
removeUndefinedProperties(options.headers);
|
|
638
|
+
const mergedOptions = mergeDeep(defaults || {}, options);
|
|
639
|
+
if (options.url === "/graphql") {
|
|
640
|
+
if (defaults && defaults.mediaType.previews?.length) {
|
|
641
|
+
mergedOptions.mediaType.previews = defaults.mediaType.previews.filter((preview) => !mergedOptions.mediaType.previews.includes(preview)).concat(mergedOptions.mediaType.previews);
|
|
642
|
+
}
|
|
643
|
+
mergedOptions.mediaType.previews = (mergedOptions.mediaType.previews || []).map((preview) => preview.replace(/-preview/, ""));
|
|
644
|
+
}
|
|
645
|
+
return mergedOptions;
|
|
646
|
+
}
|
|
647
|
+
function addQueryParameters(url, parameters) {
|
|
648
|
+
const separator = /\?/.test(url) ? "&" : "?";
|
|
649
|
+
const names = Object.keys(parameters);
|
|
650
|
+
if (names.length === 0) {
|
|
651
|
+
return url;
|
|
652
|
+
}
|
|
653
|
+
return url + separator + names.map((name) => {
|
|
654
|
+
if (name === "q") {
|
|
655
|
+
return "q=" + parameters.q.split("+").map(encodeURIComponent).join("+");
|
|
656
|
+
}
|
|
657
|
+
return `${name}=${encodeURIComponent(parameters[name])}`;
|
|
658
|
+
}).join("&");
|
|
659
|
+
}
|
|
660
|
+
var urlVariableRegex = /\{[^{}}]+\}/g;
|
|
661
|
+
function removeNonChars(variableName) {
|
|
662
|
+
return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
|
|
663
|
+
}
|
|
664
|
+
function extractUrlVariableNames(url) {
|
|
665
|
+
const matches = url.match(urlVariableRegex);
|
|
666
|
+
if (!matches) {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []);
|
|
670
|
+
}
|
|
671
|
+
function omit(object, keysToOmit) {
|
|
672
|
+
const result = { __proto__: null };
|
|
673
|
+
for (const key of Object.keys(object)) {
|
|
674
|
+
if (keysToOmit.indexOf(key) === -1) {
|
|
675
|
+
result[key] = object[key];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
function encodeReserved(str) {
|
|
681
|
+
return str.split(/(%[0-9A-Fa-f]{2})/g).map(function(part) {
|
|
682
|
+
if (!/%[0-9A-Fa-f]/.test(part)) {
|
|
683
|
+
part = encodeURI(part).replace(/%5B/g, "[").replace(/%5D/g, "]");
|
|
684
|
+
}
|
|
685
|
+
return part;
|
|
686
|
+
}).join("");
|
|
687
|
+
}
|
|
688
|
+
function encodeUnreserved(str) {
|
|
689
|
+
return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
|
|
690
|
+
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function encodeValue(operator, value, key) {
|
|
694
|
+
value = operator === "+" || operator === "#" ? encodeReserved(value) : encodeUnreserved(value);
|
|
695
|
+
if (key) {
|
|
696
|
+
return encodeUnreserved(key) + "=" + value;
|
|
697
|
+
} else {
|
|
698
|
+
return value;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function isDefined(value) {
|
|
702
|
+
return value !== undefined && value !== null;
|
|
703
|
+
}
|
|
704
|
+
function isKeyOperator(operator) {
|
|
705
|
+
return operator === ";" || operator === "&" || operator === "?";
|
|
706
|
+
}
|
|
707
|
+
function getValues(context, operator, key, modifier) {
|
|
708
|
+
var value = context[key], result = [];
|
|
709
|
+
if (isDefined(value) && value !== "") {
|
|
710
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
711
|
+
value = value.toString();
|
|
712
|
+
if (modifier && modifier !== "*") {
|
|
713
|
+
value = value.substring(0, parseInt(modifier, 10));
|
|
714
|
+
}
|
|
715
|
+
result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
|
|
716
|
+
} else {
|
|
717
|
+
if (modifier === "*") {
|
|
718
|
+
if (Array.isArray(value)) {
|
|
719
|
+
value.filter(isDefined).forEach(function(value2) {
|
|
720
|
+
result.push(encodeValue(operator, value2, isKeyOperator(operator) ? key : ""));
|
|
721
|
+
});
|
|
722
|
+
} else {
|
|
723
|
+
Object.keys(value).forEach(function(k) {
|
|
724
|
+
if (isDefined(value[k])) {
|
|
725
|
+
result.push(encodeValue(operator, value[k], k));
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
const tmp = [];
|
|
731
|
+
if (Array.isArray(value)) {
|
|
732
|
+
value.filter(isDefined).forEach(function(value2) {
|
|
733
|
+
tmp.push(encodeValue(operator, value2));
|
|
734
|
+
});
|
|
735
|
+
} else {
|
|
736
|
+
Object.keys(value).forEach(function(k) {
|
|
737
|
+
if (isDefined(value[k])) {
|
|
738
|
+
tmp.push(encodeUnreserved(k));
|
|
739
|
+
tmp.push(encodeValue(operator, value[k].toString()));
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (isKeyOperator(operator)) {
|
|
744
|
+
result.push(encodeUnreserved(key) + "=" + tmp.join(","));
|
|
745
|
+
} else if (tmp.length !== 0) {
|
|
746
|
+
result.push(tmp.join(","));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
if (operator === ";") {
|
|
752
|
+
if (isDefined(value)) {
|
|
753
|
+
result.push(encodeUnreserved(key));
|
|
754
|
+
}
|
|
755
|
+
} else if (value === "" && (operator === "&" || operator === "?")) {
|
|
756
|
+
result.push(encodeUnreserved(key) + "=");
|
|
757
|
+
} else if (value === "") {
|
|
758
|
+
result.push("");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
function parseUrl(template) {
|
|
764
|
+
return {
|
|
765
|
+
expand: expand.bind(null, template)
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function expand(template, context) {
|
|
769
|
+
var operators = ["+", "#", ".", "/", ";", "?", "&"];
|
|
770
|
+
template = template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function(_, expression, literal) {
|
|
771
|
+
if (expression) {
|
|
772
|
+
let operator = "";
|
|
773
|
+
const values = [];
|
|
774
|
+
if (operators.indexOf(expression.charAt(0)) !== -1) {
|
|
775
|
+
operator = expression.charAt(0);
|
|
776
|
+
expression = expression.substr(1);
|
|
777
|
+
}
|
|
778
|
+
expression.split(/,/g).forEach(function(variable) {
|
|
779
|
+
var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
|
|
780
|
+
values.push(getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
|
|
781
|
+
});
|
|
782
|
+
if (operator && operator !== "+") {
|
|
783
|
+
var separator = ",";
|
|
784
|
+
if (operator === "?") {
|
|
785
|
+
separator = "&";
|
|
786
|
+
} else if (operator !== "#") {
|
|
787
|
+
separator = operator;
|
|
788
|
+
}
|
|
789
|
+
return (values.length !== 0 ? operator : "") + values.join(separator);
|
|
790
|
+
} else {
|
|
791
|
+
return values.join(",");
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
return encodeReserved(literal);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
if (template === "/") {
|
|
798
|
+
return template;
|
|
799
|
+
} else {
|
|
800
|
+
return template.replace(/\/$/, "");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function parse(options) {
|
|
804
|
+
let method = options.method.toUpperCase();
|
|
805
|
+
let url = (options.url || "/").replace(/:([a-z]\w+)/g, "{$1}");
|
|
806
|
+
let headers = Object.assign({}, options.headers);
|
|
807
|
+
let body;
|
|
808
|
+
let parameters = omit(options, [
|
|
809
|
+
"method",
|
|
810
|
+
"baseUrl",
|
|
811
|
+
"url",
|
|
812
|
+
"headers",
|
|
813
|
+
"request",
|
|
814
|
+
"mediaType"
|
|
815
|
+
]);
|
|
816
|
+
const urlVariableNames = extractUrlVariableNames(url);
|
|
817
|
+
url = parseUrl(url).expand(parameters);
|
|
818
|
+
if (!/^http/.test(url)) {
|
|
819
|
+
url = options.baseUrl + url;
|
|
820
|
+
}
|
|
821
|
+
const omittedParameters = Object.keys(options).filter((option) => urlVariableNames.includes(option)).concat("baseUrl");
|
|
822
|
+
const remainingParameters = omit(parameters, omittedParameters);
|
|
823
|
+
const isBinaryRequest = /application\/octet-stream/i.test(headers.accept);
|
|
824
|
+
if (!isBinaryRequest) {
|
|
825
|
+
if (options.mediaType.format) {
|
|
826
|
+
headers.accept = headers.accept.split(/,/).map((format) => format.replace(/application\/vnd(\.\w+)(\.v3)?(\.\w+)?(\+json)?$/, `application/vnd$1$2.${options.mediaType.format}`)).join(",");
|
|
827
|
+
}
|
|
828
|
+
if (url.endsWith("/graphql")) {
|
|
829
|
+
if (options.mediaType.previews?.length) {
|
|
830
|
+
const previewsFromAcceptHeader = headers.accept.match(/(?<![\w-])[\w-]+(?=-preview)/g) || [];
|
|
831
|
+
headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => {
|
|
832
|
+
const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
|
|
833
|
+
return `application/vnd.github.${preview}-preview${format}`;
|
|
834
|
+
}).join(",");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (["GET", "HEAD"].includes(method)) {
|
|
839
|
+
url = addQueryParameters(url, remainingParameters);
|
|
840
|
+
} else {
|
|
841
|
+
if ("data" in remainingParameters) {
|
|
842
|
+
body = remainingParameters.data;
|
|
843
|
+
} else {
|
|
844
|
+
if (Object.keys(remainingParameters).length) {
|
|
845
|
+
body = remainingParameters;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (!headers["content-type"] && typeof body !== "undefined") {
|
|
850
|
+
headers["content-type"] = "application/json; charset=utf-8";
|
|
851
|
+
}
|
|
852
|
+
if (["PATCH", "PUT"].includes(method) && typeof body === "undefined") {
|
|
853
|
+
body = "";
|
|
854
|
+
}
|
|
855
|
+
return Object.assign({ method, url, headers }, typeof body !== "undefined" ? { body } : null, options.request ? { request: options.request } : null);
|
|
856
|
+
}
|
|
857
|
+
function endpointWithDefaults(defaults, route, options) {
|
|
858
|
+
return parse(merge(defaults, route, options));
|
|
859
|
+
}
|
|
860
|
+
function withDefaults(oldDefaults, newDefaults) {
|
|
861
|
+
const DEFAULTS2 = merge(oldDefaults, newDefaults);
|
|
862
|
+
const endpoint2 = endpointWithDefaults.bind(null, DEFAULTS2);
|
|
863
|
+
return Object.assign(endpoint2, {
|
|
864
|
+
DEFAULTS: DEFAULTS2,
|
|
865
|
+
defaults: withDefaults.bind(null, DEFAULTS2),
|
|
866
|
+
merge: merge.bind(null, DEFAULTS2),
|
|
867
|
+
parse
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
var endpoint = withDefaults(null, DEFAULTS);
|
|
871
|
+
|
|
872
|
+
// ../../node_modules/fast-content-type-parse/index.js
|
|
873
|
+
var NullObject = function NullObject2() {};
|
|
874
|
+
NullObject.prototype = Object.create(null);
|
|
875
|
+
var paramRE = /; *([!#$%&'*+.^\w`|~-]+)=("(?:[\v\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\v\u0020-\u00ff])*"|[!#$%&'*+.^\w`|~-]+) */gu;
|
|
876
|
+
var quotedPairRE = /\\([\v\u0020-\u00ff])/gu;
|
|
877
|
+
var mediaTypeRE = /^[!#$%&'*+.^\w|~-]+\/[!#$%&'*+.^\w|~-]+$/u;
|
|
878
|
+
var defaultContentType = { type: "", parameters: new NullObject };
|
|
879
|
+
Object.freeze(defaultContentType.parameters);
|
|
880
|
+
Object.freeze(defaultContentType);
|
|
881
|
+
function safeParse(header) {
|
|
882
|
+
if (typeof header !== "string") {
|
|
883
|
+
return defaultContentType;
|
|
884
|
+
}
|
|
885
|
+
let index = header.indexOf(";");
|
|
886
|
+
const type = index !== -1 ? header.slice(0, index).trim() : header.trim();
|
|
887
|
+
if (mediaTypeRE.test(type) === false) {
|
|
888
|
+
return defaultContentType;
|
|
889
|
+
}
|
|
890
|
+
const result = {
|
|
891
|
+
type: type.toLowerCase(),
|
|
892
|
+
parameters: new NullObject
|
|
893
|
+
};
|
|
894
|
+
if (index === -1) {
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
let key;
|
|
898
|
+
let match;
|
|
899
|
+
let value;
|
|
900
|
+
paramRE.lastIndex = index;
|
|
901
|
+
while (match = paramRE.exec(header)) {
|
|
902
|
+
if (match.index !== index) {
|
|
903
|
+
return defaultContentType;
|
|
904
|
+
}
|
|
905
|
+
index += match[0].length;
|
|
906
|
+
key = match[1].toLowerCase();
|
|
907
|
+
value = match[2];
|
|
908
|
+
if (value[0] === '"') {
|
|
909
|
+
value = value.slice(1, value.length - 1);
|
|
910
|
+
quotedPairRE.test(value) && (value = value.replace(quotedPairRE, "$1"));
|
|
911
|
+
}
|
|
912
|
+
result.parameters[key] = value;
|
|
913
|
+
}
|
|
914
|
+
if (index !== header.length) {
|
|
915
|
+
return defaultContentType;
|
|
916
|
+
}
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
var $safeParse = safeParse;
|
|
920
|
+
|
|
921
|
+
// ../../node_modules/@octokit/request-error/dist-src/index.js
|
|
922
|
+
class RequestError extends Error {
|
|
923
|
+
name;
|
|
924
|
+
status;
|
|
925
|
+
request;
|
|
926
|
+
response;
|
|
927
|
+
constructor(message, statusCode, options) {
|
|
928
|
+
super(message);
|
|
929
|
+
this.name = "HttpError";
|
|
930
|
+
this.status = Number.parseInt(statusCode);
|
|
931
|
+
if (Number.isNaN(this.status)) {
|
|
932
|
+
this.status = 0;
|
|
933
|
+
}
|
|
934
|
+
if ("response" in options) {
|
|
935
|
+
this.response = options.response;
|
|
936
|
+
}
|
|
937
|
+
const requestCopy = Object.assign({}, options.request);
|
|
938
|
+
if (options.request.headers.authorization) {
|
|
939
|
+
requestCopy.headers = Object.assign({}, options.request.headers, {
|
|
940
|
+
authorization: options.request.headers.authorization.replace(/(?<! ) .*$/, " [REDACTED]")
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
|
|
944
|
+
this.request = requestCopy;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// ../../node_modules/@octokit/request/dist-bundle/index.js
|
|
949
|
+
var VERSION2 = "9.2.4";
|
|
950
|
+
var defaults_default = {
|
|
951
|
+
headers: {
|
|
952
|
+
"user-agent": `octokit-request.js/${VERSION2} ${getUserAgent()}`
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
function isPlainObject2(value) {
|
|
956
|
+
if (typeof value !== "object" || value === null)
|
|
957
|
+
return false;
|
|
958
|
+
if (Object.prototype.toString.call(value) !== "[object Object]")
|
|
959
|
+
return false;
|
|
960
|
+
const proto = Object.getPrototypeOf(value);
|
|
961
|
+
if (proto === null)
|
|
962
|
+
return true;
|
|
963
|
+
const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
|
|
964
|
+
return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
|
|
965
|
+
}
|
|
966
|
+
async function fetchWrapper(requestOptions) {
|
|
967
|
+
const fetch = requestOptions.request?.fetch || globalThis.fetch;
|
|
968
|
+
if (!fetch) {
|
|
969
|
+
throw new Error("fetch is not set. Please pass a fetch implementation as new Octokit({ request: { fetch }}). Learn more at https://github.com/octokit/octokit.js/#fetch-missing");
|
|
970
|
+
}
|
|
971
|
+
const log = requestOptions.request?.log || console;
|
|
972
|
+
const parseSuccessResponseBody = requestOptions.request?.parseSuccessResponseBody !== false;
|
|
973
|
+
const body = isPlainObject2(requestOptions.body) || Array.isArray(requestOptions.body) ? JSON.stringify(requestOptions.body) : requestOptions.body;
|
|
974
|
+
const requestHeaders = Object.fromEntries(Object.entries(requestOptions.headers).map(([name, value]) => [
|
|
975
|
+
name,
|
|
976
|
+
String(value)
|
|
977
|
+
]));
|
|
978
|
+
let fetchResponse;
|
|
979
|
+
try {
|
|
980
|
+
fetchResponse = await fetch(requestOptions.url, {
|
|
981
|
+
method: requestOptions.method,
|
|
982
|
+
body,
|
|
983
|
+
redirect: requestOptions.request?.redirect,
|
|
984
|
+
headers: requestHeaders,
|
|
985
|
+
signal: requestOptions.request?.signal,
|
|
986
|
+
...requestOptions.body && { duplex: "half" }
|
|
987
|
+
});
|
|
988
|
+
} catch (error) {
|
|
989
|
+
let message = "Unknown Error";
|
|
990
|
+
if (error instanceof Error) {
|
|
991
|
+
if (error.name === "AbortError") {
|
|
992
|
+
error.status = 500;
|
|
993
|
+
throw error;
|
|
994
|
+
}
|
|
995
|
+
message = error.message;
|
|
996
|
+
if (error.name === "TypeError" && "cause" in error) {
|
|
997
|
+
if (error.cause instanceof Error) {
|
|
998
|
+
message = error.cause.message;
|
|
999
|
+
} else if (typeof error.cause === "string") {
|
|
1000
|
+
message = error.cause;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const requestError = new RequestError(message, 500, {
|
|
1005
|
+
request: requestOptions
|
|
1006
|
+
});
|
|
1007
|
+
requestError.cause = error;
|
|
1008
|
+
throw requestError;
|
|
1009
|
+
}
|
|
1010
|
+
const status = fetchResponse.status;
|
|
1011
|
+
const url = fetchResponse.url;
|
|
1012
|
+
const responseHeaders = {};
|
|
1013
|
+
for (const [key, value] of fetchResponse.headers) {
|
|
1014
|
+
responseHeaders[key] = value;
|
|
1015
|
+
}
|
|
1016
|
+
const octokitResponse = {
|
|
1017
|
+
url,
|
|
1018
|
+
status,
|
|
1019
|
+
headers: responseHeaders,
|
|
1020
|
+
data: ""
|
|
1021
|
+
};
|
|
1022
|
+
if ("deprecation" in responseHeaders) {
|
|
1023
|
+
const matches = responseHeaders.link && responseHeaders.link.match(/<([^<>]+)>; rel="deprecation"/);
|
|
1024
|
+
const deprecationLink = matches && matches.pop();
|
|
1025
|
+
log.warn(`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${responseHeaders.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`);
|
|
1026
|
+
}
|
|
1027
|
+
if (status === 204 || status === 205) {
|
|
1028
|
+
return octokitResponse;
|
|
1029
|
+
}
|
|
1030
|
+
if (requestOptions.method === "HEAD") {
|
|
1031
|
+
if (status < 400) {
|
|
1032
|
+
return octokitResponse;
|
|
1033
|
+
}
|
|
1034
|
+
throw new RequestError(fetchResponse.statusText, status, {
|
|
1035
|
+
response: octokitResponse,
|
|
1036
|
+
request: requestOptions
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
if (status === 304) {
|
|
1040
|
+
octokitResponse.data = await getResponseData(fetchResponse);
|
|
1041
|
+
throw new RequestError("Not modified", status, {
|
|
1042
|
+
response: octokitResponse,
|
|
1043
|
+
request: requestOptions
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
if (status >= 400) {
|
|
1047
|
+
octokitResponse.data = await getResponseData(fetchResponse);
|
|
1048
|
+
throw new RequestError(toErrorMessage(octokitResponse.data), status, {
|
|
1049
|
+
response: octokitResponse,
|
|
1050
|
+
request: requestOptions
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
octokitResponse.data = parseSuccessResponseBody ? await getResponseData(fetchResponse) : fetchResponse.body;
|
|
1054
|
+
return octokitResponse;
|
|
1055
|
+
}
|
|
1056
|
+
async function getResponseData(response) {
|
|
1057
|
+
const contentType = response.headers.get("content-type");
|
|
1058
|
+
if (!contentType) {
|
|
1059
|
+
return response.text().catch(() => "");
|
|
1060
|
+
}
|
|
1061
|
+
const mimetype = $safeParse(contentType);
|
|
1062
|
+
if (isJSONResponse(mimetype)) {
|
|
1063
|
+
let text = "";
|
|
1064
|
+
try {
|
|
1065
|
+
text = await response.text();
|
|
1066
|
+
return JSON.parse(text);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
return text;
|
|
1069
|
+
}
|
|
1070
|
+
} else if (mimetype.type.startsWith("text/") || mimetype.parameters.charset?.toLowerCase() === "utf-8") {
|
|
1071
|
+
return response.text().catch(() => "");
|
|
1072
|
+
} else {
|
|
1073
|
+
return response.arrayBuffer().catch(() => new ArrayBuffer(0));
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function isJSONResponse(mimetype) {
|
|
1077
|
+
return mimetype.type === "application/json" || mimetype.type === "application/scim+json";
|
|
1078
|
+
}
|
|
1079
|
+
function toErrorMessage(data) {
|
|
1080
|
+
if (typeof data === "string") {
|
|
1081
|
+
return data;
|
|
1082
|
+
}
|
|
1083
|
+
if (data instanceof ArrayBuffer) {
|
|
1084
|
+
return "Unknown error";
|
|
1085
|
+
}
|
|
1086
|
+
if ("message" in data) {
|
|
1087
|
+
const suffix = "documentation_url" in data ? ` - ${data.documentation_url}` : "";
|
|
1088
|
+
return Array.isArray(data.errors) ? `${data.message}: ${data.errors.map((v) => JSON.stringify(v)).join(", ")}${suffix}` : `${data.message}${suffix}`;
|
|
1089
|
+
}
|
|
1090
|
+
return `Unknown error: ${JSON.stringify(data)}`;
|
|
1091
|
+
}
|
|
1092
|
+
function withDefaults2(oldEndpoint, newDefaults) {
|
|
1093
|
+
const endpoint2 = oldEndpoint.defaults(newDefaults);
|
|
1094
|
+
const newApi = function(route, parameters) {
|
|
1095
|
+
const endpointOptions = endpoint2.merge(route, parameters);
|
|
1096
|
+
if (!endpointOptions.request || !endpointOptions.request.hook) {
|
|
1097
|
+
return fetchWrapper(endpoint2.parse(endpointOptions));
|
|
1098
|
+
}
|
|
1099
|
+
const request2 = (route2, parameters2) => {
|
|
1100
|
+
return fetchWrapper(endpoint2.parse(endpoint2.merge(route2, parameters2)));
|
|
1101
|
+
};
|
|
1102
|
+
Object.assign(request2, {
|
|
1103
|
+
endpoint: endpoint2,
|
|
1104
|
+
defaults: withDefaults2.bind(null, endpoint2)
|
|
1105
|
+
});
|
|
1106
|
+
return endpointOptions.request.hook(request2, endpointOptions);
|
|
1107
|
+
};
|
|
1108
|
+
return Object.assign(newApi, {
|
|
1109
|
+
endpoint: endpoint2,
|
|
1110
|
+
defaults: withDefaults2.bind(null, endpoint2)
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
var request = withDefaults2(endpoint, defaults_default);
|
|
1114
|
+
|
|
1115
|
+
// ../../node_modules/@octokit/graphql/dist-bundle/index.js
|
|
1116
|
+
var VERSION3 = "0.0.0-development";
|
|
1117
|
+
function _buildMessageForResponseErrors(data) {
|
|
1118
|
+
return `Request failed due to following response errors:
|
|
1119
|
+
` + data.errors.map((e) => ` - ${e.message}`).join(`
|
|
1120
|
+
`);
|
|
1121
|
+
}
|
|
1122
|
+
var GraphqlResponseError = class extends Error {
|
|
1123
|
+
constructor(request2, headers, response) {
|
|
1124
|
+
super(_buildMessageForResponseErrors(response));
|
|
1125
|
+
this.request = request2;
|
|
1126
|
+
this.headers = headers;
|
|
1127
|
+
this.response = response;
|
|
1128
|
+
this.errors = response.errors;
|
|
1129
|
+
this.data = response.data;
|
|
1130
|
+
if (Error.captureStackTrace) {
|
|
1131
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
name = "GraphqlResponseError";
|
|
1135
|
+
errors;
|
|
1136
|
+
data;
|
|
1137
|
+
};
|
|
1138
|
+
var NON_VARIABLE_OPTIONS = [
|
|
1139
|
+
"method",
|
|
1140
|
+
"baseUrl",
|
|
1141
|
+
"url",
|
|
1142
|
+
"headers",
|
|
1143
|
+
"request",
|
|
1144
|
+
"query",
|
|
1145
|
+
"mediaType",
|
|
1146
|
+
"operationName"
|
|
1147
|
+
];
|
|
1148
|
+
var FORBIDDEN_VARIABLE_OPTIONS = ["query", "method", "url"];
|
|
1149
|
+
var GHES_V3_SUFFIX_REGEX = /\/api\/v3\/?$/;
|
|
1150
|
+
function graphql(request2, query, options) {
|
|
1151
|
+
if (options) {
|
|
1152
|
+
if (typeof query === "string" && "query" in options) {
|
|
1153
|
+
return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`));
|
|
1154
|
+
}
|
|
1155
|
+
for (const key in options) {
|
|
1156
|
+
if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key))
|
|
1157
|
+
continue;
|
|
1158
|
+
return Promise.reject(new Error(`[@octokit/graphql] "${key}" cannot be used as variable name`));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const parsedOptions = typeof query === "string" ? Object.assign({ query }, options) : query;
|
|
1162
|
+
const requestOptions = Object.keys(parsedOptions).reduce((result, key) => {
|
|
1163
|
+
if (NON_VARIABLE_OPTIONS.includes(key)) {
|
|
1164
|
+
result[key] = parsedOptions[key];
|
|
1165
|
+
return result;
|
|
1166
|
+
}
|
|
1167
|
+
if (!result.variables) {
|
|
1168
|
+
result.variables = {};
|
|
1169
|
+
}
|
|
1170
|
+
result.variables[key] = parsedOptions[key];
|
|
1171
|
+
return result;
|
|
1172
|
+
}, {});
|
|
1173
|
+
const baseUrl = parsedOptions.baseUrl || request2.endpoint.DEFAULTS.baseUrl;
|
|
1174
|
+
if (GHES_V3_SUFFIX_REGEX.test(baseUrl)) {
|
|
1175
|
+
requestOptions.url = baseUrl.replace(GHES_V3_SUFFIX_REGEX, "/api/graphql");
|
|
1176
|
+
}
|
|
1177
|
+
return request2(requestOptions).then((response) => {
|
|
1178
|
+
if (response.data.errors) {
|
|
1179
|
+
const headers = {};
|
|
1180
|
+
for (const key of Object.keys(response.headers)) {
|
|
1181
|
+
headers[key] = response.headers[key];
|
|
1182
|
+
}
|
|
1183
|
+
throw new GraphqlResponseError(requestOptions, headers, response.data);
|
|
1184
|
+
}
|
|
1185
|
+
return response.data.data;
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
function withDefaults3(request2, newDefaults) {
|
|
1189
|
+
const newRequest = request2.defaults(newDefaults);
|
|
1190
|
+
const newApi = (query, options) => {
|
|
1191
|
+
return graphql(newRequest, query, options);
|
|
1192
|
+
};
|
|
1193
|
+
return Object.assign(newApi, {
|
|
1194
|
+
defaults: withDefaults3.bind(null, newRequest),
|
|
1195
|
+
endpoint: newRequest.endpoint
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
var graphql2 = withDefaults3(request, {
|
|
1199
|
+
headers: {
|
|
1200
|
+
"user-agent": `octokit-graphql.js/${VERSION3} ${getUserAgent()}`
|
|
1201
|
+
},
|
|
1202
|
+
method: "POST",
|
|
1203
|
+
url: "/graphql"
|
|
1204
|
+
});
|
|
1205
|
+
// ../../packages/aeg-forge-state/src/github-token.ts
|
|
1206
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1207
|
+
import { promisify as promisify2 } from "node:util";
|
|
1208
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1209
|
+
var systemEnv2 = {
|
|
1210
|
+
...process.env,
|
|
1211
|
+
PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
|
|
1212
|
+
};
|
|
1213
|
+
async function resolveGithubToken(explicit) {
|
|
1214
|
+
if (explicit && explicit.length > 0)
|
|
1215
|
+
return explicit;
|
|
1216
|
+
if (process.env.GITHUB_TOKEN)
|
|
1217
|
+
return process.env.GITHUB_TOKEN;
|
|
1218
|
+
if (process.env.GH_TOKEN)
|
|
1219
|
+
return process.env.GH_TOKEN;
|
|
1220
|
+
try {
|
|
1221
|
+
const { stdout } = await execFileAsync2("gh", ["auth", "token"], { env: systemEnv2, timeout: 5000 });
|
|
1222
|
+
const token = stdout.trim();
|
|
1223
|
+
return token.length > 0 ? token : null;
|
|
1224
|
+
} catch {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// ../../packages/aeg-forge-state/src/map-forge-facts.ts
|
|
1230
|
+
function mapForgeFacts(raw) {
|
|
1231
|
+
if (!raw.issue)
|
|
1232
|
+
return null;
|
|
1233
|
+
return {
|
|
1234
|
+
issueState: raw.issue.state === "OPEN" ? "open" : "closed",
|
|
1235
|
+
assigned: raw.issue.assigneesCount > 0,
|
|
1236
|
+
blockedLabel: hasLabel("blocked", raw.issue.labels),
|
|
1237
|
+
branchExists: raw.refExists,
|
|
1238
|
+
prState: mapPrState(raw.pullRequest?.state),
|
|
1239
|
+
reviewDecision: mapReviewDecision(raw.pullRequest?.reviewDecision),
|
|
1240
|
+
stateReason: mapStateReason(raw.issue.stateReason),
|
|
1241
|
+
closedAt: raw.issue.closedAt ?? null,
|
|
1242
|
+
mergedAt: raw.pullRequest?.mergedAt ?? null,
|
|
1243
|
+
closedByActor: raw.closedByActor ?? null
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function mapStateReason(reason) {
|
|
1247
|
+
if (reason === "COMPLETED")
|
|
1248
|
+
return "completed";
|
|
1249
|
+
if (reason === "NOT_PLANNED")
|
|
1250
|
+
return "not_planned";
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
function mapPrState(state) {
|
|
1254
|
+
if (state === "OPEN")
|
|
1255
|
+
return "open";
|
|
1256
|
+
if (state === "MERGED")
|
|
1257
|
+
return "merged";
|
|
1258
|
+
return "none";
|
|
1259
|
+
}
|
|
1260
|
+
function mapReviewDecision(decision) {
|
|
1261
|
+
if (decision === "APPROVED")
|
|
1262
|
+
return "approved";
|
|
1263
|
+
if (decision === "CHANGES_REQUESTED")
|
|
1264
|
+
return "changes_requested";
|
|
1265
|
+
return "none";
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// ../../packages/aeg-forge-state/src/fetch-forge-facts.ts
|
|
1269
|
+
function buildBranchName(tranche, taskId) {
|
|
1270
|
+
return `task/${tranche}/${taskId}`;
|
|
1271
|
+
}
|
|
1272
|
+
async function fetchForgeFacts(input) {
|
|
1273
|
+
const token = await resolveGithubToken(input.token);
|
|
1274
|
+
if (!token) {
|
|
1275
|
+
return {
|
|
1276
|
+
facts: new Map,
|
|
1277
|
+
prRefs: new Map,
|
|
1278
|
+
unavailable: true,
|
|
1279
|
+
reason: "No GitHub token available (set GITHUB_TOKEN/GH_TOKEN or `gh auth login`)."
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const queriedTasks = input.tasks.filter((t) => t.issue !== null);
|
|
1283
|
+
if (queriedTasks.length === 0) {
|
|
1284
|
+
return { facts: new Map, prRefs: new Map, unavailable: false };
|
|
1285
|
+
}
|
|
1286
|
+
const client = graphql2.defaults({ headers: { authorization: `bearer ${token}` } });
|
|
1287
|
+
const query = buildBatchQuery(input.tranche, queriedTasks);
|
|
1288
|
+
let response;
|
|
1289
|
+
try {
|
|
1290
|
+
response = await client(query, { owner: input.owner, repo: input.repo });
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
return {
|
|
1293
|
+
facts: new Map,
|
|
1294
|
+
prRefs: new Map,
|
|
1295
|
+
unavailable: true,
|
|
1296
|
+
reason: `GitHub query failed: ${describeError(err)}`
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
const facts = new Map;
|
|
1300
|
+
const prRefs = new Map;
|
|
1301
|
+
if (!response.repository) {
|
|
1302
|
+
return {
|
|
1303
|
+
facts,
|
|
1304
|
+
prRefs,
|
|
1305
|
+
unavailable: true,
|
|
1306
|
+
reason: `Repository ${input.owner}/${input.repo} not visible to this token.`
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
for (const task of queriedTasks) {
|
|
1310
|
+
const alias = aliasFor(task.id);
|
|
1311
|
+
const raw = extractRawFromResponse(response.repository, alias);
|
|
1312
|
+
const mapped = mapForgeFacts(raw);
|
|
1313
|
+
if (mapped)
|
|
1314
|
+
facts.set(task.id, mapped);
|
|
1315
|
+
const pr = raw.pullRequest;
|
|
1316
|
+
if (pr && typeof pr.number === "number" && typeof pr.url === "string") {
|
|
1317
|
+
prRefs.set(task.id, { number: pr.number, url: pr.url, state: pr.state });
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return { facts, prRefs, unavailable: false };
|
|
1321
|
+
}
|
|
1322
|
+
function buildBatchQuery(tranche, tasks) {
|
|
1323
|
+
const perTask = tasks.map((task) => {
|
|
1324
|
+
const a = aliasFor(task.id);
|
|
1325
|
+
const branch = buildBranchName(tranche, task.id);
|
|
1326
|
+
return `
|
|
1327
|
+
${a}_issue: issue(number: ${task.issue}) {
|
|
1328
|
+
state
|
|
1329
|
+
stateReason
|
|
1330
|
+
closedAt
|
|
1331
|
+
assignees(first: 1) { totalCount }
|
|
1332
|
+
labels(first: 50) { nodes { name } }
|
|
1333
|
+
timelineItems(last: 1, itemTypes: [CLOSED_EVENT]) {
|
|
1334
|
+
nodes {
|
|
1335
|
+
... on ClosedEvent {
|
|
1336
|
+
actor { login }
|
|
1337
|
+
closer {
|
|
1338
|
+
... on PullRequest {
|
|
1339
|
+
number
|
|
1340
|
+
url
|
|
1341
|
+
state
|
|
1342
|
+
reviewDecision
|
|
1343
|
+
mergedAt
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
${a}_ref: ref(qualifiedName: ${JSON.stringify(`refs/heads/${branch}`)}) {
|
|
1351
|
+
name
|
|
1352
|
+
}
|
|
1353
|
+
${a}_prs: pullRequests(
|
|
1354
|
+
first: 1,
|
|
1355
|
+
headRefName: ${JSON.stringify(branch)},
|
|
1356
|
+
orderBy: { field: CREATED_AT, direction: DESC }
|
|
1357
|
+
) {
|
|
1358
|
+
nodes { number url state reviewDecision mergedAt }
|
|
1359
|
+
}`;
|
|
1360
|
+
}).join("");
|
|
1361
|
+
return `query ForgeFacts($owner: String!, $repo: String!) {
|
|
1362
|
+
repository(owner: $owner, name: $repo) {${perTask}
|
|
1363
|
+
}
|
|
1364
|
+
}`;
|
|
1365
|
+
}
|
|
1366
|
+
function aliasFor(taskId) {
|
|
1367
|
+
const sanitized = taskId.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1368
|
+
return `t_${sanitized}`;
|
|
1369
|
+
}
|
|
1370
|
+
function extractRawFromResponse(repository, alias) {
|
|
1371
|
+
const issue = repository[`${alias}_issue`];
|
|
1372
|
+
const ref = repository[`${alias}_ref`];
|
|
1373
|
+
const prs = repository[`${alias}_prs`];
|
|
1374
|
+
const timelineNode = issue?.timelineItems?.nodes?.[0];
|
|
1375
|
+
const rawCloser = timelineNode?.closer ?? null;
|
|
1376
|
+
const closingPr = rawCloser && typeof rawCloser.number === "number" && typeof rawCloser.url === "string" ? rawCloser : null;
|
|
1377
|
+
const branchPr = prs && prs.nodes.length > 0 && prs.nodes[0] ? prs.nodes[0] : null;
|
|
1378
|
+
return {
|
|
1379
|
+
issue: issue ? {
|
|
1380
|
+
state: issue.state,
|
|
1381
|
+
stateReason: issue.stateReason,
|
|
1382
|
+
closedAt: issue.closedAt ?? null,
|
|
1383
|
+
assigneesCount: issue.assignees.totalCount,
|
|
1384
|
+
labels: issue.labels.nodes.map((n) => n.name)
|
|
1385
|
+
} : null,
|
|
1386
|
+
refExists: Boolean(ref && ref.name.length > 0),
|
|
1387
|
+
pullRequest: closingPr ?? branchPr,
|
|
1388
|
+
closedByActor: timelineNode?.actor?.login ?? null
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function describeError(err) {
|
|
1392
|
+
if (err instanceof Error)
|
|
1393
|
+
return err.message;
|
|
1394
|
+
if (typeof err === "string")
|
|
1395
|
+
return err;
|
|
1396
|
+
return "unknown error";
|
|
1397
|
+
}
|
|
1398
|
+
// ../../packages/aeg-forge-state/src/fetch-open-issues.ts
|
|
1399
|
+
async function fetchOpenIssuesByLabel(slugs, owner, repo, token) {
|
|
1400
|
+
const result = new Map;
|
|
1401
|
+
if (slugs.length === 0)
|
|
1402
|
+
return result;
|
|
1403
|
+
const client = graphql2.defaults({ headers: { authorization: `bearer ${token}` } });
|
|
1404
|
+
const toAlias = (slug) => `tranche_${slug.replace(/-/g, "_")}`;
|
|
1405
|
+
const perSlug = slugs.map((slug) => `
|
|
1406
|
+
${toAlias(slug)}: issues(states: [OPEN], labels: [${JSON.stringify(trancheLabel(slug))}], first: 100) {
|
|
1407
|
+
nodes { number body labels(first: 20) { nodes { name } } }
|
|
1408
|
+
}`).join("");
|
|
1409
|
+
const query = `query LabeledIssues($owner: String!, $repo: String!) {
|
|
1410
|
+
repository(owner: $owner, name: $repo) {${perSlug}
|
|
1411
|
+
}
|
|
1412
|
+
}`;
|
|
1413
|
+
let response;
|
|
1414
|
+
try {
|
|
1415
|
+
response = await client(query, { owner, repo });
|
|
1416
|
+
} catch {
|
|
1417
|
+
return result;
|
|
1418
|
+
}
|
|
1419
|
+
if (!response.repository)
|
|
1420
|
+
return result;
|
|
1421
|
+
for (const slug of slugs) {
|
|
1422
|
+
const conn = response.repository[toAlias(slug)];
|
|
1423
|
+
const issues = (conn?.nodes ?? []).map((n) => ({
|
|
1424
|
+
number: n.number,
|
|
1425
|
+
body: n.body ?? "",
|
|
1426
|
+
labels: n.labels?.nodes?.map((l) => l.name) ?? []
|
|
1427
|
+
}));
|
|
1428
|
+
result.set(slug, issues);
|
|
1429
|
+
}
|
|
1430
|
+
return result;
|
|
1431
|
+
}
|
|
1432
|
+
// ../../packages/aeg-forge-state/src/fetch-task-issue-refs.ts
|
|
1433
|
+
async function fetchTaskIssueRefs(owner, repo, issueNumbers, token) {
|
|
1434
|
+
const result = new Map;
|
|
1435
|
+
if (issueNumbers.length === 0)
|
|
1436
|
+
return result;
|
|
1437
|
+
const resolvedToken = await resolveGithubToken(token);
|
|
1438
|
+
if (!resolvedToken)
|
|
1439
|
+
return result;
|
|
1440
|
+
const client = graphql2.defaults({ headers: { authorization: `bearer ${resolvedToken}` } });
|
|
1441
|
+
let response;
|
|
1442
|
+
try {
|
|
1443
|
+
response = await client(buildQuery(issueNumbers), { owner, repo });
|
|
1444
|
+
} catch {
|
|
1445
|
+
return result;
|
|
1446
|
+
}
|
|
1447
|
+
if (!response.repository)
|
|
1448
|
+
return result;
|
|
1449
|
+
for (const n of issueNumbers) {
|
|
1450
|
+
const node = response.repository[aliasFor2(n)];
|
|
1451
|
+
if (!node)
|
|
1452
|
+
continue;
|
|
1453
|
+
const labels = node.labels.nodes.map((l) => l.name);
|
|
1454
|
+
result.set(n, resolveTaskIssueRef(node.title, labels));
|
|
1455
|
+
}
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
function aliasFor2(n) {
|
|
1459
|
+
return `i_${n}`;
|
|
1460
|
+
}
|
|
1461
|
+
function buildQuery(issueNumbers) {
|
|
1462
|
+
const perIssue = issueNumbers.map((n) => `
|
|
1463
|
+
${aliasFor2(n)}: issue(number: ${n}) {
|
|
1464
|
+
title
|
|
1465
|
+
labels(first: 50) { nodes { name } }
|
|
1466
|
+
}`).join("");
|
|
1467
|
+
return `query TaskIssueRefs($owner: String!, $repo: String!) {
|
|
1468
|
+
repository(owner: $owner, name: $repo) {${perIssue}
|
|
1469
|
+
}
|
|
1470
|
+
}`;
|
|
1471
|
+
}
|
|
1472
|
+
// ../../packages/aeg-forge-state/src/resolve-repo.ts
|
|
1473
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1474
|
+
import { promisify as promisify3 } from "node:util";
|
|
1475
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
1476
|
+
var systemEnv3 = {
|
|
1477
|
+
...process.env,
|
|
1478
|
+
PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
|
|
1479
|
+
};
|
|
1480
|
+
// ../../packages/aeg-core/src/anchored-region.ts
|
|
1481
|
+
function anchoredRegionBounds(body, field) {
|
|
1482
|
+
const masked = maskCode(body);
|
|
1483
|
+
const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
|
|
1484
|
+
if (!start)
|
|
1485
|
+
return null;
|
|
1486
|
+
const innerStart = start.index + start[0].length;
|
|
1487
|
+
const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(innerStart));
|
|
1488
|
+
if (!end)
|
|
1489
|
+
return null;
|
|
1490
|
+
const innerEnd = innerStart + end.index;
|
|
1491
|
+
return { outerStart: start.index, outerEnd: innerEnd + end[0].length, innerStart, innerEnd };
|
|
1492
|
+
}
|
|
1493
|
+
function anchoredRegion(body, field) {
|
|
1494
|
+
const bounds = anchoredRegionBounds(body, field);
|
|
1495
|
+
if (!bounds)
|
|
1496
|
+
return null;
|
|
1497
|
+
return body.slice(bounds.innerStart, bounds.innerEnd);
|
|
1498
|
+
}
|
|
1499
|
+
// ../../packages/aeg-core/src/parse-registry.ts
|
|
1500
|
+
function parseRegistry(md) {
|
|
1501
|
+
const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
|
|
1502
|
+
const projects = [];
|
|
1503
|
+
for (const row of rows) {
|
|
1504
|
+
if (row.length < 4)
|
|
1505
|
+
continue;
|
|
1506
|
+
const name = stripBackticks(row[0] ?? "").trim();
|
|
1507
|
+
const path = stripBackticks(row[1] ?? "").trim();
|
|
1508
|
+
const specsPath = stripBackticks(row[2] ?? "").trim();
|
|
1509
|
+
const stateCell = (row[3] ?? "").trim();
|
|
1510
|
+
if (!name || !path || !specsPath)
|
|
1511
|
+
continue;
|
|
1512
|
+
projects.push({
|
|
1513
|
+
name,
|
|
1514
|
+
path,
|
|
1515
|
+
specsPath,
|
|
1516
|
+
statePath: parseStateCell(stateCell)
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
return projects;
|
|
1520
|
+
}
|
|
1521
|
+
function parseStateCell(cell) {
|
|
1522
|
+
if (!cell)
|
|
1523
|
+
return null;
|
|
1524
|
+
const stripped = stripBackticks(cell).trim();
|
|
1525
|
+
if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
|
|
1526
|
+
return stripped;
|
|
1527
|
+
}
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
function stripBackticks(s) {
|
|
1531
|
+
return s.replace(/`/g, "");
|
|
1532
|
+
}
|
|
1533
|
+
function readMarkdownTable(md, headingMatcher) {
|
|
1534
|
+
const lines = md.split(/\r?\n/);
|
|
1535
|
+
let i = 0;
|
|
1536
|
+
for (;i < lines.length; i++) {
|
|
1537
|
+
if (headingMatcher.test(lines[i] ?? "")) {
|
|
1538
|
+
i++;
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (i >= lines.length)
|
|
1543
|
+
return [];
|
|
1544
|
+
for (;i < lines.length; i++) {
|
|
1545
|
+
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
if (i + 1 >= lines.length)
|
|
1549
|
+
return [];
|
|
1550
|
+
i += 2;
|
|
1551
|
+
const out = [];
|
|
1552
|
+
for (;i < lines.length; i++) {
|
|
1553
|
+
const trimmed = (lines[i] ?? "").trim();
|
|
1554
|
+
if (!trimmed.startsWith("|"))
|
|
1555
|
+
break;
|
|
1556
|
+
out.push(splitTableRow(trimmed));
|
|
1557
|
+
}
|
|
1558
|
+
return out;
|
|
1559
|
+
}
|
|
1560
|
+
function splitTableRow(row) {
|
|
1561
|
+
const PLACEHOLDER = "\x00PIPE\x00";
|
|
1562
|
+
const escaped = row.replace(/\\\|/g, PLACEHOLDER);
|
|
1563
|
+
const parts = escaped.split("|").map((s) => s.trim());
|
|
1564
|
+
if (parts.length && parts[0] === "")
|
|
1565
|
+
parts.shift();
|
|
1566
|
+
if (parts.length && parts[parts.length - 1] === "")
|
|
1567
|
+
parts.pop();
|
|
1568
|
+
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
1569
|
+
}
|
|
1570
|
+
// ../../packages/aeg-core/src/state-machine-model.ts
|
|
1571
|
+
var FORGE_FACT_INPUTS = [
|
|
1572
|
+
{
|
|
1573
|
+
fact: "issueState",
|
|
1574
|
+
readsFrom: "Issue.state (OPEN | CLOSED)",
|
|
1575
|
+
meaning: "Whether the task Issue is still open — the anchor for the honest terminal statuses."
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
fact: "assigned",
|
|
1579
|
+
readsFrom: "Issue.assignees (count > 0)",
|
|
1580
|
+
meaning: "An assignee exists. No longer affects derivation: assigned and unassigned are both todo."
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
fact: "branchExists",
|
|
1584
|
+
readsFrom: "Ref refs/heads/task/<tranche>/<id>",
|
|
1585
|
+
meaning: "A task branch has been published — the todo → in-flight transition, written by git push, not by hand."
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
fact: "prState",
|
|
1589
|
+
readsFrom: "PullRequest.state (OPEN | CLOSED | MERGED)",
|
|
1590
|
+
meaning: "Whether work is proposed or landed. CLOSED-without-merge collapses to none — AEG models open/merged/none."
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
fact: "reviewDecision",
|
|
1594
|
+
readsFrom: "PullRequest.reviewDecision",
|
|
1595
|
+
meaning: "Only 'changes_requested' flips status; approved-but-unmerged stays in-review."
|
|
1596
|
+
},
|
|
1597
|
+
{
|
|
1598
|
+
fact: "blockedLabel",
|
|
1599
|
+
readsFrom: `Issue.labels contains '${label("blocked")}'`,
|
|
1600
|
+
meaning: "Execution halted pending an external unblock. Wins over every other rule."
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
fact: "stateReason",
|
|
1604
|
+
readsFrom: "Issue.stateReason (COMPLETED | NOT_PLANNED | REOPENED | null)",
|
|
1605
|
+
meaning: "Separates a legitimate drop from an incoherent close on a closed, never-merged Issue."
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
fact: "closedAt",
|
|
1609
|
+
readsFrom: "Issue.closedAt",
|
|
1610
|
+
meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
fact: "mergedAt",
|
|
1614
|
+
readsFrom: "PullRequest.mergedAt",
|
|
1615
|
+
meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
|
|
1616
|
+
},
|
|
1617
|
+
{
|
|
1618
|
+
fact: "closedByActor",
|
|
1619
|
+
readsFrom: "ClosedEvent.actor.login (most recent timeline CLOSED_EVENT)",
|
|
1620
|
+
meaning: "GitHub login that hand-closed the Issue. No task-status derivation rule reads it — it is consumed by dispatch-gate.ts/coherence-checks.ts A1 to recognize a hand-closed dependency as resolved (task vinaya-engine-v1 21, #99), not by this status model."
|
|
1621
|
+
}
|
|
1622
|
+
];
|
|
1623
|
+
var DERIVED_STATUSES = [
|
|
1624
|
+
"backlog",
|
|
1625
|
+
"todo",
|
|
1626
|
+
"in-flight",
|
|
1627
|
+
"in-review",
|
|
1628
|
+
"changes-requested",
|
|
1629
|
+
"merged",
|
|
1630
|
+
"blocked",
|
|
1631
|
+
"dropped",
|
|
1632
|
+
"incoherent"
|
|
1633
|
+
];
|
|
1634
|
+
var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
|
|
1635
|
+
// ../../packages/aeg-core/src/file-classify.ts
|
|
1636
|
+
function isCodeFile(p) {
|
|
1637
|
+
if (p.endsWith(".md"))
|
|
1638
|
+
return false;
|
|
1639
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
|
|
1640
|
+
}
|
|
1641
|
+
// ../../packages/aeg-core/src/waiver-label.ts
|
|
1642
|
+
var WAIVER_LABEL = label("waiver-docs");
|
|
1643
|
+
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
1644
|
+
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
1645
|
+
function isPrincipal(login, principalAllowlist) {
|
|
1646
|
+
if (login === null)
|
|
1647
|
+
return false;
|
|
1648
|
+
const normalized = login.toLowerCase();
|
|
1649
|
+
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
1650
|
+
}
|
|
1651
|
+
function isWaiverLabelActorVerified(opts) {
|
|
1652
|
+
if (!opts.labels.includes(opts.label))
|
|
1653
|
+
return false;
|
|
1654
|
+
return isPrincipal(opts.labelActor, opts.principalAllowlist);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// ../../packages/aeg-core/src/doc-owners.ts
|
|
1658
|
+
var DOC_OWNERS_PATH = ".vinaya/doc-owners";
|
|
1659
|
+
function parseDocOwners(content) {
|
|
1660
|
+
const bindings = [];
|
|
1661
|
+
const errors = [];
|
|
1662
|
+
const lines = content.split(`
|
|
1663
|
+
`);
|
|
1664
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1665
|
+
const raw = lines[i] ?? "";
|
|
1666
|
+
const stripped = raw.replace(/#.*$/, "").trim();
|
|
1667
|
+
if (!stripped)
|
|
1668
|
+
continue;
|
|
1669
|
+
const parts = stripped.split(/\s+/);
|
|
1670
|
+
if (parts.length < 2) {
|
|
1671
|
+
errors.push(`C5 doc-owners-parse: ${DOC_OWNERS_PATH}:${i + 1} — malformed binding (expected "<glob> <pointer>", got "${raw.trim()}").`);
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
const [glob, ...pointerParts] = parts;
|
|
1675
|
+
bindings.push({ glob: glob ?? "", pointer: pointerParts.join(" "), lineNum: i + 1 });
|
|
1676
|
+
}
|
|
1677
|
+
return { bindings, errors };
|
|
1678
|
+
}
|
|
1679
|
+
function globToRegex(pat) {
|
|
1680
|
+
let re = "^";
|
|
1681
|
+
let i = 0;
|
|
1682
|
+
while (i < pat.length) {
|
|
1683
|
+
const c = pat[i];
|
|
1684
|
+
if (c === "*") {
|
|
1685
|
+
if (pat[i + 1] === "*") {
|
|
1686
|
+
re += ".*";
|
|
1687
|
+
i += 2;
|
|
1688
|
+
} else {
|
|
1689
|
+
re += "[^/]*";
|
|
1690
|
+
i += 1;
|
|
1691
|
+
}
|
|
1692
|
+
} else if (/[.+^$|(){}[\]\\?]/.test(c)) {
|
|
1693
|
+
re += `\\${c}`;
|
|
1694
|
+
i += 1;
|
|
1695
|
+
} else {
|
|
1696
|
+
re += c;
|
|
1697
|
+
i += 1;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
re += "$";
|
|
1701
|
+
return new RegExp(re);
|
|
1702
|
+
}
|
|
1703
|
+
function isUrlPointer(p) {
|
|
1704
|
+
return /^https?:\/\//.test(p);
|
|
1705
|
+
}
|
|
1706
|
+
function pointerToPath(p) {
|
|
1707
|
+
const idx = p.indexOf("#");
|
|
1708
|
+
return idx === -1 ? p : p.slice(0, idx);
|
|
1709
|
+
}
|
|
1710
|
+
var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
|
|
1711
|
+
function readPointerNoteField(body, field) {
|
|
1712
|
+
const out = [];
|
|
1713
|
+
const re = new RegExp(`^[ \\t]*${field}[ \\t]*:[ \\t]*(.+?)${SEPARATOR}(.+?)[ \\t]*$`, "gim");
|
|
1714
|
+
for (const m of body.matchAll(re)) {
|
|
1715
|
+
out.push({ surface: (m[1] ?? "").trim(), note: (m[2] ?? "").trim() });
|
|
1716
|
+
}
|
|
1717
|
+
return out;
|
|
1718
|
+
}
|
|
1719
|
+
function readDocAcks(body) {
|
|
1720
|
+
return readPointerNoteField(body, "Doc-ack");
|
|
1721
|
+
}
|
|
1722
|
+
function readDocNeutrals(body) {
|
|
1723
|
+
return readPointerNoteField(body, "Doc-neutral");
|
|
1724
|
+
}
|
|
1725
|
+
var LANGUAGE_COMMENT_PREFIXES = [
|
|
1726
|
+
{ test: /\.(ts|tsx|js|jsx|mjs|cjs)$/, prefixes: ["//"] },
|
|
1727
|
+
{ test: /(^|\/)(pre-push|pre-commit|pre-merge-commit)$/, prefixes: ["#"] },
|
|
1728
|
+
{ test: /\.(sh|bash)$/, prefixes: ["#"] },
|
|
1729
|
+
{ test: /\.ya?ml$/, prefixes: ["#"] },
|
|
1730
|
+
{ test: /\.py$/, prefixes: ["#"] }
|
|
1731
|
+
];
|
|
1732
|
+
function commentPrefixesForPath(path) {
|
|
1733
|
+
for (const { test, prefixes } of LANGUAGE_COMMENT_PREFIXES) {
|
|
1734
|
+
if (test.test(path))
|
|
1735
|
+
return prefixes;
|
|
1736
|
+
}
|
|
1737
|
+
return [];
|
|
1738
|
+
}
|
|
1739
|
+
function isNeutralDiffContentLine(line, prefixes) {
|
|
1740
|
+
const t = line.trim();
|
|
1741
|
+
if (t === "")
|
|
1742
|
+
return true;
|
|
1743
|
+
if (t.startsWith("#!"))
|
|
1744
|
+
return false;
|
|
1745
|
+
return prefixes.some((p) => t.startsWith(p));
|
|
1746
|
+
}
|
|
1747
|
+
function isMechanicallyNeutralDiff(diffText, path) {
|
|
1748
|
+
const prefixes = commentPrefixesForPath(path);
|
|
1749
|
+
let sawChange = false;
|
|
1750
|
+
for (const raw of diffText.split(`
|
|
1751
|
+
`)) {
|
|
1752
|
+
if (raw.startsWith("+++ ") || raw.startsWith("--- ") || raw.startsWith("@@"))
|
|
1753
|
+
continue;
|
|
1754
|
+
if (raw.startsWith("+") || raw.startsWith("-")) {
|
|
1755
|
+
if (!isNeutralDiffContentLine(raw.slice(1), prefixes))
|
|
1756
|
+
return false;
|
|
1757
|
+
sawChange = true;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return sawChange;
|
|
1761
|
+
}
|
|
1762
|
+
function evaluateC5(changed, docOwnersContent, prBody, fileExists, waiverActive, getDiff) {
|
|
1763
|
+
const out = { errors: [], notes: [] };
|
|
1764
|
+
if (docOwnersContent === null)
|
|
1765
|
+
return out;
|
|
1766
|
+
const { bindings, errors: parseErrors } = parseDocOwners(docOwnersContent);
|
|
1767
|
+
for (const e of parseErrors)
|
|
1768
|
+
out.errors.push(e);
|
|
1769
|
+
if (bindings.length === 0)
|
|
1770
|
+
return out;
|
|
1771
|
+
const codeFiles = changed.filter(isCodeFile);
|
|
1772
|
+
const fired = [];
|
|
1773
|
+
for (const b of bindings) {
|
|
1774
|
+
const re = globToRegex(b.glob);
|
|
1775
|
+
const matchedFiles = codeFiles.filter((f) => re.test(f));
|
|
1776
|
+
if (matchedFiles.length > 0)
|
|
1777
|
+
fired.push({ binding: b, matchedFiles });
|
|
1778
|
+
}
|
|
1779
|
+
if (fired.length === 0)
|
|
1780
|
+
return out;
|
|
1781
|
+
const acks = readDocAcks(prBody);
|
|
1782
|
+
const neutrals = readDocNeutrals(prBody);
|
|
1783
|
+
for (const { binding: b, matchedFiles } of fired) {
|
|
1784
|
+
if (waiverActive) {
|
|
1785
|
+
out.notes.push(`C5 doc-waiver active for ${b.pointer} (binding ${DOC_OWNERS_PATH}:${b.lineNum})`);
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
if (isUrlPointer(b.pointer)) {
|
|
1789
|
+
const acked = acks.some((a) => a.surface === b.pointer);
|
|
1790
|
+
if (!acked) {
|
|
1791
|
+
out.errors.push(`C5 doc-coverage: code change matched ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}). External pointer requires \`Doc-ack: ${b.pointer} — <note>\` in the PR body, or an actor-verified \`${WAIVER_LABEL}\` label to skip.`);
|
|
1792
|
+
}
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const pointerPath = pointerToPath(b.pointer);
|
|
1796
|
+
if (!fileExists(pointerPath)) {
|
|
1797
|
+
out.errors.push(`C5 doc-owners-dangling: ${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Fix the binding or add the doc.`);
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
if (changed.includes(pointerPath))
|
|
1801
|
+
continue;
|
|
1802
|
+
const declared = neutrals.find((n) => n.surface === b.pointer);
|
|
1803
|
+
if (declared) {
|
|
1804
|
+
const evidenced = getDiff !== undefined && matchedFiles.every((f) => {
|
|
1805
|
+
const diff = getDiff(f);
|
|
1806
|
+
return diff !== null && isMechanicallyNeutralDiff(diff, f);
|
|
1807
|
+
});
|
|
1808
|
+
if (evidenced) {
|
|
1809
|
+
out.notes.push(`C5 doc-neutral: ${b.pointer} not required for ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\`) — declared neutral ("${declared.note}"), diff of ${matchedFiles.join(", ")} confirmed comment/whitespace-only.`);
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
out.errors.push(`C5 doc-neutral-unverified: ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}) declared \`Doc-neutral: ${b.pointer} — ${declared.note}\`, but the diff of ${matchedFiles.join(", ")} contains changes beyond comments/whitespace. Update ${pointerPath}, or have a principal apply the \`${WAIVER_LABEL}\` label.`);
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
out.errors.push(`C5 doc-coverage: code change matched ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}), but ${pointerPath} is not in the PR diff. Update it, declare \`Doc-neutral: ${b.pointer} — <why>\` if the change is genuinely mechanically-neutral, or have a principal apply the \`${WAIVER_LABEL}\` label.`);
|
|
1816
|
+
}
|
|
1817
|
+
return out;
|
|
1818
|
+
}
|
|
1819
|
+
// ../../packages/aeg-core/src/pr-tier.ts
|
|
1820
|
+
var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
|
|
1821
|
+
function readTierFromPrBody(prBody) {
|
|
1822
|
+
const searchIn = anchoredRegion(prBody, "TIER") ?? prBody;
|
|
1823
|
+
const m = searchIn.match(TIER_FIELD);
|
|
1824
|
+
if (!m)
|
|
1825
|
+
return null;
|
|
1826
|
+
const t = Number(m[4]);
|
|
1827
|
+
return t === 0 || t === 1 || t === 3 ? t : null;
|
|
1828
|
+
}
|
|
1829
|
+
var OVERRIDE_BODY_TOKEN = `[${label("override-docs")}]`;
|
|
1830
|
+
function overrideActive(opts) {
|
|
1831
|
+
if (opts.overrideDocsEnv === "1")
|
|
1832
|
+
return true;
|
|
1833
|
+
const labels = (opts.prLabels || "").split(",").map((s) => s.trim());
|
|
1834
|
+
if (hasLabel("override-docs", labels))
|
|
1835
|
+
return true;
|
|
1836
|
+
if ((opts.prBody || "").includes(OVERRIDE_BODY_TOKEN))
|
|
1837
|
+
return true;
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
// ../../packages/aeg-core/src/premise-check.ts
|
|
1841
|
+
var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
|
|
1842
|
+
|
|
1843
|
+
// ../../packages/aeg-core/src/test-plan-section.ts
|
|
1844
|
+
var HEADING_FORM_RE = /^#{1,4}[ \t]*(?:\d+[a-z]?\.[ \t]*)?\*{0,2}Test Plan\*{0,2}[ \t]*$/im;
|
|
1845
|
+
var INLINE_FORM_RE = /^[ \t]*\*{0,2}Test Plan:?\*{0,2}/im;
|
|
1846
|
+
var NEXT_SECTION_RE = /\n(?:#{1,3}[ \t]|\*\*[A-Z][^*\n]*:\*\*)/;
|
|
1847
|
+
function locateTestPlanSection(body) {
|
|
1848
|
+
const region = anchoredRegion(body, "TEST-PLAN");
|
|
1849
|
+
if (region !== null)
|
|
1850
|
+
return { found: true, section: region };
|
|
1851
|
+
const headingMatch = HEADING_FORM_RE.exec(body);
|
|
1852
|
+
const match = headingMatch ?? INLINE_FORM_RE.exec(body);
|
|
1853
|
+
if (!match || match.index === undefined)
|
|
1854
|
+
return { found: false };
|
|
1855
|
+
const sectionBody = body.slice(match.index);
|
|
1856
|
+
const firstLineEnd = sectionBody.indexOf(`
|
|
1857
|
+
`);
|
|
1858
|
+
const searchFrom = firstLineEnd === -1 ? sectionBody.length : firstLineEnd + 1;
|
|
1859
|
+
const nextMatch = sectionBody.slice(searchFrom).match(NEXT_SECTION_RE);
|
|
1860
|
+
const sectionEnd = nextMatch ? searchFrom + (nextMatch.index ?? 0) : sectionBody.length;
|
|
1861
|
+
return { found: true, section: sectionBody.slice(0, sectionEnd) };
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// ../../packages/aeg-core/src/brief-validation.ts
|
|
1865
|
+
function headerRegion(prBody) {
|
|
1866
|
+
const m = prBody.match(/^##\s/m);
|
|
1867
|
+
return m?.index !== undefined ? prBody.slice(0, m.index) : prBody;
|
|
1868
|
+
}
|
|
1869
|
+
function headerField(prBody, labelPattern, anchor) {
|
|
1870
|
+
const anchored = anchor !== undefined ? anchoredRegion(prBody, anchor) : null;
|
|
1871
|
+
const region = anchored ?? headerRegion(prBody);
|
|
1872
|
+
const re = new RegExp(`^(?:\\*\\*)?\\s*${labelPattern}\\s*(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([^\\n·]+)`, "im");
|
|
1873
|
+
const m = region.match(re);
|
|
1874
|
+
if (!m)
|
|
1875
|
+
return null;
|
|
1876
|
+
const value = m[1].trim();
|
|
1877
|
+
return value.length > 0 ? value : null;
|
|
1878
|
+
}
|
|
1879
|
+
function normalize(text) {
|
|
1880
|
+
return text.replace(/[*_]/g, "").replace(/\s+/g, " ");
|
|
1881
|
+
}
|
|
1882
|
+
function headingCheck(prBody, keywordPattern, sectionName) {
|
|
1883
|
+
const re = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?(?:\\d+[a-z]?\\.\\s*)?[^\\n]*${keywordPattern}`, "im");
|
|
1884
|
+
if (re.test(prBody))
|
|
1885
|
+
return { status: "pass", errors: [] };
|
|
1886
|
+
return {
|
|
1887
|
+
status: "fail",
|
|
1888
|
+
errors: [`brief-validation ${sectionName}: no "${sectionName}" section found in the PR body.`]
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function checkTierField(prBody, readTier) {
|
|
1892
|
+
if (readTier(prBody) !== null)
|
|
1893
|
+
return { status: "pass", errors: [] };
|
|
1894
|
+
return {
|
|
1895
|
+
status: "fail",
|
|
1896
|
+
errors: [
|
|
1897
|
+
"brief-validation tier: no `Tier:` field found in the PR body (expected `Tier: 0|1|3` or `**Tier:** 0|1|3`)."
|
|
1898
|
+
]
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
var TEST_PLAN_UNIT_TESTS_ONLY_RE = /(?:\*\*)?Test Plan(?:\*\*)?\s*:\s*(?:\*\*)?\s*unit-tests-only/i;
|
|
1902
|
+
function testPlanRegion(prBody) {
|
|
1903
|
+
const located = locateTestPlanSection(prBody);
|
|
1904
|
+
return located.found ? located.section : prBody;
|
|
1905
|
+
}
|
|
1906
|
+
function checkTestPlan(prBody) {
|
|
1907
|
+
const region = testPlanRegion(prBody);
|
|
1908
|
+
if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region))
|
|
1909
|
+
return { status: "pass", errors: [] };
|
|
1910
|
+
if (/\*\*\[(?:agent|principal)\]\*\*/.test(region))
|
|
1911
|
+
return { status: "pass", errors: [] };
|
|
1912
|
+
return {
|
|
1913
|
+
status: "fail",
|
|
1914
|
+
errors: [
|
|
1915
|
+
"brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
|
|
1916
|
+
]
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function checkTestPlanExclusivity(prBody) {
|
|
1920
|
+
const region = testPlanRegion(prBody);
|
|
1921
|
+
if (!TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region))
|
|
1922
|
+
return { status: "pass", errors: [] };
|
|
1923
|
+
if (/^-\s*\[[ xX]\]\s*\*{2}\[(?:agent|principal)\]\*{2}/im.test(region)) {
|
|
1924
|
+
return {
|
|
1925
|
+
status: "fail",
|
|
1926
|
+
errors: [
|
|
1927
|
+
"brief-validation Test Plan shape: `Test Plan: unit-tests-only` and a `- [ ]`/`- [x]` tagged checkbox item are mutually exclusive (brief-authoring §9) — declare one form, not both."
|
|
1928
|
+
]
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
return { status: "pass", errors: [] };
|
|
1932
|
+
}
|
|
1933
|
+
function checkPrincipalPlaceholder(prBody) {
|
|
1934
|
+
const region = testPlanRegion(prBody);
|
|
1935
|
+
const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
|
|
1936
|
+
for (const m of region.matchAll(lineRe)) {
|
|
1937
|
+
const content = m[1] ?? "";
|
|
1938
|
+
if (/^\s*None\b/i.test(content)) {
|
|
1939
|
+
return {
|
|
1940
|
+
status: "fail",
|
|
1941
|
+
errors: [
|
|
1942
|
+
'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
|
|
1943
|
+
]
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return { status: "pass", errors: [] };
|
|
1948
|
+
}
|
|
1949
|
+
function checkSurfaceMap(prBody) {
|
|
1950
|
+
return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
|
|
1951
|
+
}
|
|
1952
|
+
function checkDocUpdateList(prBody) {
|
|
1953
|
+
return headingCheck(prBody, "(?:documentation|doc)[- ]update(?:\\s+list)?", "Documentation-update list");
|
|
1954
|
+
}
|
|
1955
|
+
function checkWorktreeStep0(prBody) {
|
|
1956
|
+
if (/git worktree add/.test(prBody))
|
|
1957
|
+
return { status: "pass", errors: [] };
|
|
1958
|
+
return {
|
|
1959
|
+
status: "fail",
|
|
1960
|
+
errors: ["brief-validation worktree Step 0: no `git worktree add` command found in the PR body."]
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
function checkStopConditions(prBody) {
|
|
1964
|
+
return headingCheck(prBody, "stop conditions", "Stop conditions");
|
|
1965
|
+
}
|
|
1966
|
+
function checkAutonomyClause(prBody) {
|
|
1967
|
+
const normalized = normalize(prBody).toLowerCase();
|
|
1968
|
+
if (/do not stop to ask clarifying questions/.test(normalized))
|
|
1969
|
+
return { status: "pass", errors: [] };
|
|
1970
|
+
return {
|
|
1971
|
+
status: "fail",
|
|
1972
|
+
errors: [
|
|
1973
|
+
'brief-validation autonomy clause: the standing autonomy clause ("Do not stop to ask clarifying questions...") was not found in the PR body.'
|
|
1974
|
+
]
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
function checkProjectField(prBody) {
|
|
1978
|
+
if (headerField(prBody, "Project(?:\\(s\\))?", "PROJECT") !== null)
|
|
1979
|
+
return { status: "pass", errors: [] };
|
|
1980
|
+
return {
|
|
1981
|
+
status: "fail",
|
|
1982
|
+
errors: [
|
|
1983
|
+
"brief-validation Project: no `Project:` field found in the PR body header block (before the first `##` heading). Required per the brief-developer contract — expected `Project: <name>[, <name>]` or `**Project:** …`."
|
|
1984
|
+
]
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function checkForField(prBody) {
|
|
1988
|
+
if (headerField(prBody, "For") !== null)
|
|
1989
|
+
return { status: "pass", errors: [] };
|
|
1990
|
+
return {
|
|
1991
|
+
status: "fail",
|
|
1992
|
+
errors: [
|
|
1993
|
+
"brief-validation For: no `For:` field found in the PR body header block (before the first `##` heading). Required per the brief-authoring skill — expected `For: <model + environment>` or `**For:** …`."
|
|
1994
|
+
]
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
function checkClosesN(prBody) {
|
|
1998
|
+
const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
|
|
1999
|
+
if (closesPattern.test(stripCode(prBody))) {
|
|
2000
|
+
return { status: "pass", errors: [] };
|
|
2001
|
+
}
|
|
2002
|
+
if (closesPattern.test(prBody)) {
|
|
2003
|
+
return {
|
|
2004
|
+
status: "fail",
|
|
2005
|
+
errors: [
|
|
2006
|
+
"brief-validation Closes #N: `Closes #N` found only inside a code span — GitHub won't auto-close it. Put a bare `Closes #N` on its own line inside the `AEG:CLOSES` anchor."
|
|
2007
|
+
]
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
return {
|
|
2011
|
+
status: "fail",
|
|
2012
|
+
errors: ["brief-validation Closes #N: no `Closes #<N>` (or Fixes/Resolves) reference found in the PR body."]
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
var BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause];
|
|
2016
|
+
var TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/;
|
|
2017
|
+
function isTaskBranch(branch) {
|
|
2018
|
+
return TASK_BRANCH_PATTERN.test(branch);
|
|
2019
|
+
}
|
|
2020
|
+
function isBriefShaped(prBody) {
|
|
2021
|
+
const stripped = stripCode(prBody);
|
|
2022
|
+
return BRIEF_SHAPE_MARKERS.filter((check) => check(stripped).status === "pass").length >= 2;
|
|
2023
|
+
}
|
|
2024
|
+
function checkBriefSections(prBody, readTier, options = {}) {
|
|
2025
|
+
const { requireClosesN = true } = options;
|
|
2026
|
+
const results = [
|
|
2027
|
+
checkTierField(prBody, readTier),
|
|
2028
|
+
checkTestPlan(prBody),
|
|
2029
|
+
checkTestPlanExclusivity(prBody),
|
|
2030
|
+
checkPrincipalPlaceholder(prBody),
|
|
2031
|
+
checkSurfaceMap(prBody),
|
|
2032
|
+
checkDocUpdateList(prBody),
|
|
2033
|
+
checkWorktreeStep0(prBody),
|
|
2034
|
+
checkStopConditions(prBody),
|
|
2035
|
+
checkAutonomyClause(prBody),
|
|
2036
|
+
checkProjectField(prBody),
|
|
2037
|
+
checkForField(prBody),
|
|
2038
|
+
...requireClosesN ? [checkClosesN(prBody)] : []
|
|
2039
|
+
];
|
|
2040
|
+
return { errors: results.flatMap((r) => r.errors) };
|
|
2041
|
+
}
|
|
2042
|
+
// ../../packages/aeg-core/src/issue-validation.ts
|
|
2043
|
+
function hasRationaleField(body, labelPattern) {
|
|
2044
|
+
const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})`, "im");
|
|
2045
|
+
return re.test(body);
|
|
2046
|
+
}
|
|
2047
|
+
var RATIONALE_FIELDS = [
|
|
2048
|
+
{ name: "Boundary", pattern: "Boundary" },
|
|
2049
|
+
{ name: "Sizing", pattern: "Sizing" },
|
|
2050
|
+
{ name: "Project(s) + blast radius", pattern: "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius" },
|
|
2051
|
+
{ name: "Dependency rationale", pattern: "Dependency rationale|Depends[- ]on" },
|
|
2052
|
+
{ name: "Traps to avoid", pattern: "Traps" },
|
|
2053
|
+
{ name: "Suggested agent-class", pattern: "(?:Suggested\\s+)?agent-class" },
|
|
2054
|
+
{ name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
|
|
2055
|
+
{ name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
|
|
2056
|
+
];
|
|
2057
|
+
var DEPENDENCY_RATIONALE_FIELD_NAME = "Dependency rationale";
|
|
2058
|
+
function checkIssueRationale(body) {
|
|
2059
|
+
const errors = [];
|
|
2060
|
+
for (const f of RATIONALE_FIELDS) {
|
|
2061
|
+
if (!hasRationaleField(body, f.pattern)) {
|
|
2062
|
+
errors.push(`issue-validation ${f.name}: rationale field not found in the Issue body — every task Issue carries the full Planner's rationale (aeg-root/contracts/planner-brief.md).`);
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (f.name === DEPENDENCY_RATIONALE_FIELD_NAME && !SECTION_HEADER.test(body)) {
|
|
2066
|
+
errors.push(`issue-validation ${f.name}: rationale field found, but not in the form amend-deps requires — ` + "write `**Dependency rationale** — …`, not `**Dependency rationale:** …`. " + "`amendRationaleDeps` (the only sanctioned way to edit Depends-on/Conflicts-with) locates this " + "section by the exact anchor `**Dependency rationale**`; a colon inside the bold breaks that match " + "and the Issue becomes unamendable.");
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
2070
|
+
}
|
|
2071
|
+
function isTaskIssueLabelSet(labels) {
|
|
2072
|
+
return hasLabel("tranche", labels);
|
|
2073
|
+
}
|
|
2074
|
+
function isControlCodePoint(codePoint) {
|
|
2075
|
+
return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
|
|
2076
|
+
}
|
|
2077
|
+
function isDisplayControlCodePoint(codePoint) {
|
|
2078
|
+
return codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297 || codePoint === 65279;
|
|
2079
|
+
}
|
|
2080
|
+
var MESSAGE_VALUE_COUNT_MAX = 5;
|
|
2081
|
+
function residueForMessage(values) {
|
|
2082
|
+
const shown = values.slice(0, MESSAGE_VALUE_COUNT_MAX).map((v) => `\`${forMessage(v)}\``);
|
|
2083
|
+
const rest = values.length - shown.length;
|
|
2084
|
+
return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
|
|
2085
|
+
}
|
|
2086
|
+
var MESSAGE_VALUE_MAX = 64;
|
|
2087
|
+
function forMessage(value) {
|
|
2088
|
+
const kept = [];
|
|
2089
|
+
for (const ch of value.replace(/\s+/g, " ")) {
|
|
2090
|
+
const codePoint = ch.codePointAt(0) ?? 0;
|
|
2091
|
+
if (isControlCodePoint(codePoint) || isDisplayControlCodePoint(codePoint))
|
|
2092
|
+
continue;
|
|
2093
|
+
kept.push(ch);
|
|
2094
|
+
}
|
|
2095
|
+
const cleaned = kept.join("").trim();
|
|
2096
|
+
if (cleaned.length === 0)
|
|
2097
|
+
return "(unprintable)";
|
|
2098
|
+
const points = Array.from(cleaned);
|
|
2099
|
+
return points.length > MESSAGE_VALUE_MAX ? `${points.slice(0, MESSAGE_VALUE_MAX).join("")}…` : cleaned;
|
|
2100
|
+
}
|
|
2101
|
+
function checkProjectsRegistered(body, _labels, registeredNames) {
|
|
2102
|
+
if (registeredNames.length === 0)
|
|
2103
|
+
return { status: "pass", errors: [] };
|
|
2104
|
+
const field = projectFieldFromBody(body);
|
|
2105
|
+
if (!field.declared)
|
|
2106
|
+
return { status: "pass", errors: [] };
|
|
2107
|
+
const known = new Set(registeredNames.map((n) => n.trim()));
|
|
2108
|
+
const registeredList = [...registeredNames].map(forMessage).join(", ");
|
|
2109
|
+
const errors = [];
|
|
2110
|
+
if (field.unreadable) {
|
|
2111
|
+
return {
|
|
2112
|
+
status: "fail",
|
|
2113
|
+
errors: [
|
|
2114
|
+
`issue-validation project registry: the body has an unterminated code fence, so its \`**Project:**\` field could not be read reliably — the fence runs to end of body (CommonMark, and how the forge renders it), swallowing everything after it including the foot field where the declaration lives by convention. Read from the raw body the field says ${residueForMessage(field.unparsed) || "(nothing)"}, but a line inside a swallowed region may be a quoted example rather than a real declaration, so this gate refuses rather than guessing which. Balance the fences — every opening run of backticks or tildes needs a closing run at least as long — and the field reads normally.`
|
|
2115
|
+
]
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
const unregistered = field.names.filter((p) => !known.has(p));
|
|
2119
|
+
if (unregistered.length > 0) {
|
|
2120
|
+
const caseHints = unregistered.map((p) => {
|
|
2121
|
+
const row = [...known].find((k) => k.toLowerCase() === p.toLowerCase());
|
|
2122
|
+
return row ? `\`${forMessage(p)}\` differs from the registered \`${forMessage(row)}\` only in case` : null;
|
|
2123
|
+
}).filter((h) => h !== null);
|
|
2124
|
+
errors.push(`issue-validation project registry: the \`**Project:**\` field declares ${unregistered.map(forMessage).join(", ")} — no such row in \`.vinaya/projects.md\`, which is the authority for valid project names (registered: ${registeredList}). Fix the name, or register the project with \`vinaya init product <name> --path <folder>\` first; an unregistered project has no specs to read and no per-project state to update.${caseHints.length > 0 ? ` Note: ${caseHints.join("; ")} — project names are matched exactly, because every downstream consumer compares them literally.` : ""} This reads the same field \`projectsFromBody\` derives the task's project from, so a name here that is not a row is a task that resolves to a project that does not exist.`);
|
|
2125
|
+
}
|
|
2126
|
+
if (field.names.length === 0) {
|
|
2127
|
+
const residue = field.unparsed.length > 0 ? ` — ${residueForMessage(field.unparsed)}` : " — the field is empty";
|
|
2128
|
+
errors.push(`issue-validation project registry: the \`**Project:**\` field is present but resolves to no project name${residue} (registered: ${registeredList}). A project name is a slug (\`[a-z0-9][a-z0-9-]*\`, matched exactly); prose, a parenthetical, or a sentence in this field resolves to no project at all, and this gate cannot check a name it never receives — which is how a declaration like this used to pass. Write the registered name on its own, or register the project with \`vinaya init product <name> --path <folder>\` first. If the task genuinely touches no registered project, omit the field rather than explaining its absence inside it — \`checkIssueRationale\` already requires the \`Project(s) + blast radius\` narrative field for that.`);
|
|
2129
|
+
} else if (field.unparsed.length > 0) {
|
|
2130
|
+
errors.push(`issue-validation project registry: the \`**Project:**\` field declares a value that is not a project name — ${residueForMessage(field.unparsed)} (registered: ${registeredList}). The rest of the field parsed, so this is a name the gate silently could not check rather than a field it could not read at all. A project name is a slug (\`[a-z0-9][a-z0-9-]*\`, matched exactly); write it on its own, or drop it if it names no project.`);
|
|
2131
|
+
}
|
|
2132
|
+
return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
2136
|
+
var COHERENCE_ENFORCED_FROM = "2026-07-01";
|
|
2137
|
+
function isGrandfathered(isoDate) {
|
|
2138
|
+
if (!isoDate)
|
|
2139
|
+
return false;
|
|
2140
|
+
return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM;
|
|
2141
|
+
}
|
|
2142
|
+
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
2143
|
+
function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
|
|
2144
|
+
const failures = [];
|
|
2145
|
+
for (const e of entries) {
|
|
2146
|
+
if (!e.facts)
|
|
2147
|
+
continue;
|
|
2148
|
+
if (e.facts.stateReason === "not_planned")
|
|
2149
|
+
continue;
|
|
2150
|
+
if (e.facts.issueState === "closed" && e.facts.prState !== "merged") {
|
|
2151
|
+
const handClosed = e.facts.stateReason === "completed" && isPrincipal(e.facts.closedByActor, principalAllowlist);
|
|
2152
|
+
if (handClosed)
|
|
2153
|
+
continue;
|
|
2154
|
+
failures.push({
|
|
2155
|
+
issue: e.task.issue,
|
|
2156
|
+
tranche: e.trancheSlug,
|
|
2157
|
+
task: e.task.id,
|
|
2158
|
+
reason: `Issue closed but closing PR is not merged (prState: ${e.facts.prState})`,
|
|
2159
|
+
grandfathered: isGrandfathered(e.facts.closedAt)
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const activeFails = failures.filter((f) => !f.grandfathered);
|
|
2164
|
+
const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
|
|
2165
|
+
return {
|
|
2166
|
+
check: "A1",
|
|
2167
|
+
status,
|
|
2168
|
+
failures,
|
|
2169
|
+
note: status === "info" ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function checkA3(entries) {
|
|
2173
|
+
const failures = [];
|
|
2174
|
+
for (const e of entries) {
|
|
2175
|
+
if (!e.facts)
|
|
2176
|
+
continue;
|
|
2177
|
+
if (e.facts.prState === "merged" && e.facts.issueState !== "closed") {
|
|
2178
|
+
failures.push({
|
|
2179
|
+
issue: e.task.issue,
|
|
2180
|
+
tranche: e.trancheSlug,
|
|
2181
|
+
task: e.task.id,
|
|
2182
|
+
reason: "Closing PR is merged but Issue is still open (GitHub auto-close misfire)",
|
|
2183
|
+
grandfathered: isGrandfathered(e.facts.mergedAt)
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
const activeFails = failures.filter((f) => !f.grandfathered);
|
|
2188
|
+
const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
|
|
2189
|
+
return {
|
|
2190
|
+
check: "A3",
|
|
2191
|
+
status,
|
|
2192
|
+
failures,
|
|
2193
|
+
note: status === "info" ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
function checkT1(entries) {
|
|
2197
|
+
const failures = [];
|
|
2198
|
+
for (const e of entries) {
|
|
2199
|
+
if (e.task.issue === null)
|
|
2200
|
+
continue;
|
|
2201
|
+
if (e.facts === undefined) {
|
|
2202
|
+
failures.push({
|
|
2203
|
+
issue: e.task.issue,
|
|
2204
|
+
tranche: e.trancheSlug,
|
|
2205
|
+
task: e.task.id,
|
|
2206
|
+
reason: `Issue #${e.task.issue} in topology does not resolve to a real GitHub Issue`
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return { check: "T1", status: failures.length > 0 ? "fail" : "pass", failures };
|
|
2211
|
+
}
|
|
2212
|
+
function checkT2(openIssuesBySlug, topologyIssuesBySlug, ciTrancheSlug) {
|
|
2213
|
+
const failures = [];
|
|
2214
|
+
for (const [slug, openNums] of openIssuesBySlug) {
|
|
2215
|
+
if (ciTrancheSlug && slug !== ciTrancheSlug)
|
|
2216
|
+
continue;
|
|
2217
|
+
const topologySet = topologyIssuesBySlug.get(slug) ?? new Set;
|
|
2218
|
+
for (const num of openNums) {
|
|
2219
|
+
if (!topologySet.has(num)) {
|
|
2220
|
+
failures.push({
|
|
2221
|
+
issue: num,
|
|
2222
|
+
tranche: slug,
|
|
2223
|
+
reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return { check: "T2", status: failures.length > 0 ? "fail" : "pass", failures };
|
|
2229
|
+
}
|
|
2230
|
+
function scopeT2ToPlanPr(result, isPlanPr) {
|
|
2231
|
+
if (isPlanPr || result.status !== "fail")
|
|
2232
|
+
return result;
|
|
2233
|
+
return {
|
|
2234
|
+
...result,
|
|
2235
|
+
status: "info",
|
|
2236
|
+
note: result.note ?? "T2 findings are non-blocking outside plan PRs (point-of-power principle) — see aeg-root/enforcement.md."
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs) {
|
|
2240
|
+
const preEnforcement = new Set;
|
|
2241
|
+
if (enrichedEntries) {
|
|
2242
|
+
for (const e of enrichedEntries) {
|
|
2243
|
+
if (isGrandfathered(e.facts?.closedAt) || isGrandfathered(e.facts?.mergedAt)) {
|
|
2244
|
+
preEnforcement.add(e.trancheSlug);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
const failures = [];
|
|
2249
|
+
for (const e of entries) {
|
|
2250
|
+
if (!e.archived && e.task.issue === null) {
|
|
2251
|
+
if (ciTrancheSlug && e.trancheSlug !== ciTrancheSlug)
|
|
2252
|
+
continue;
|
|
2253
|
+
if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
|
|
2254
|
+
failures.push({
|
|
2255
|
+
issue: null,
|
|
2256
|
+
tranche: e.trancheSlug,
|
|
2257
|
+
task: e.task.id,
|
|
2258
|
+
reason: `Task ${e.task.id} in active tranche has no Issue ref (#TBD or empty), but forge data for tranche "${e.trancheSlug}" was unavailable — cannot evaluate grandfather status, not silently failed`,
|
|
2259
|
+
grandfathered: true
|
|
2260
|
+
});
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
failures.push({
|
|
2264
|
+
issue: null,
|
|
2265
|
+
tranche: e.trancheSlug,
|
|
2266
|
+
task: e.task.id,
|
|
2267
|
+
reason: `Task ${e.task.id} in active tranche has no Issue ref (#TBD or empty) — the model requires all active tasks to have Issue numbers`,
|
|
2268
|
+
grandfathered: preEnforcement.has(e.trancheSlug)
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const activeFails = failures.filter((f) => !f.grandfathered);
|
|
2273
|
+
const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
|
|
2274
|
+
return {
|
|
2275
|
+
check: "T3",
|
|
2276
|
+
status,
|
|
2277
|
+
failures,
|
|
2278
|
+
note: status === "info" ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function checkD1(entries, issueToEntry, taskToEntry) {
|
|
2282
|
+
const failures = [];
|
|
2283
|
+
for (const e of entries) {
|
|
2284
|
+
if (!e.facts)
|
|
2285
|
+
continue;
|
|
2286
|
+
if (e.facts.prState !== "open")
|
|
2287
|
+
continue;
|
|
2288
|
+
for (const dep of e.task.dependsOn) {
|
|
2289
|
+
const depEntry = resolveDepEntry(dep, e.trancheSlug, issueToEntry, taskToEntry);
|
|
2290
|
+
if (!depEntry)
|
|
2291
|
+
continue;
|
|
2292
|
+
const depFacts = depEntry.facts;
|
|
2293
|
+
const depClosed = depFacts?.issueState === "closed";
|
|
2294
|
+
if (!depClosed) {
|
|
2295
|
+
failures.push({
|
|
2296
|
+
issue: e.task.issue,
|
|
2297
|
+
tranche: e.trancheSlug,
|
|
2298
|
+
task: e.task.id,
|
|
2299
|
+
reason: `Task has open PR but depends-on ${dep} (issue #${depEntry.task.issue ?? "?"}) is not closed`
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return { check: "D1", status: failures.length > 0 ? "fail" : "pass", failures };
|
|
2305
|
+
}
|
|
2306
|
+
function resolveDepEntry(dep, trancheSlug, issueToEntry, taskToEntry) {
|
|
2307
|
+
const issueMatch = dep.match(/^#(\d+)$/);
|
|
2308
|
+
if (issueMatch?.[1])
|
|
2309
|
+
return issueToEntry.get(Number(issueMatch[1]));
|
|
2310
|
+
return taskToEntry.get(`${trancheSlug}/${dep}`) ?? taskToEntry.get(dep);
|
|
2311
|
+
}
|
|
2312
|
+
function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
|
|
2313
|
+
const failures = [];
|
|
2314
|
+
for (const [slug, issues] of issuesBySlug) {
|
|
2315
|
+
for (const issue of issues) {
|
|
2316
|
+
if (!isTaskIssueLabelSet(issue.labels))
|
|
2317
|
+
continue;
|
|
2318
|
+
const errors = [
|
|
2319
|
+
...checkIssueRationale(issue.body).errors,
|
|
2320
|
+
...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors
|
|
2321
|
+
];
|
|
2322
|
+
if (errors.length === 0)
|
|
2323
|
+
continue;
|
|
2324
|
+
failures.push({
|
|
2325
|
+
issue: issue.number,
|
|
2326
|
+
tranche: slug,
|
|
2327
|
+
reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(" | ")}`,
|
|
2328
|
+
grandfathered: grandfatheredIssues.has(issue.number)
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
const activeFails = failures.filter((f) => !f.grandfathered);
|
|
2333
|
+
const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
|
|
2334
|
+
return {
|
|
2335
|
+
check: "R1",
|
|
2336
|
+
status,
|
|
2337
|
+
failures,
|
|
2338
|
+
note: status === "info" ? `${failures.length} grandfathered task Issue(s) predate this check's grammar (rationale fields and/or the project registry)` : undefined
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function checkL1(files, entriesBySlug) {
|
|
2342
|
+
const failures = [];
|
|
2343
|
+
for (const f of files) {
|
|
2344
|
+
if (f.archived)
|
|
2345
|
+
continue;
|
|
2346
|
+
const entries = entriesBySlug.get(f.slug) ?? [];
|
|
2347
|
+
const withFacts = entries.filter((e) => e.facts !== undefined);
|
|
2348
|
+
if (withFacts.length === 0)
|
|
2349
|
+
continue;
|
|
2350
|
+
const allClosed = withFacts.every((e) => e.facts?.issueState === "closed");
|
|
2351
|
+
if (allClosed) {
|
|
2352
|
+
failures.push({
|
|
2353
|
+
tranche: f.slug,
|
|
2354
|
+
reason: "Active tranche has no open task-Issues — consider archiving to completed/"
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
check: "L1",
|
|
2360
|
+
status: "info",
|
|
2361
|
+
failures,
|
|
2362
|
+
note: failures.length > 0 ? `${failures.length} active tranche(s) with no open task-Issues — consider archiving (advisory)` : undefined
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
function checkL3(files) {
|
|
2366
|
+
const active = files.filter((f) => !f.archived);
|
|
2367
|
+
return {
|
|
2368
|
+
check: "L3",
|
|
2369
|
+
status: "info",
|
|
2370
|
+
failures: [],
|
|
2371
|
+
note: `${active.length} active tranche(s): ${active.map((f) => f.slug).join(", ") || "(none)"}`
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
function extractClosesReferences(prBody) {
|
|
2375
|
+
const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#(\d+)/gi;
|
|
2376
|
+
const stripped = stripCode(prBody);
|
|
2377
|
+
const searchIn = anchoredRegion(stripped, "CLOSES") ?? stripped;
|
|
2378
|
+
const referenced = new Set;
|
|
2379
|
+
for (const hit of searchIn.matchAll(closesPattern)) {
|
|
2380
|
+
referenced.add(Number(hit[1]));
|
|
2381
|
+
}
|
|
2382
|
+
return referenced;
|
|
2383
|
+
}
|
|
2384
|
+
function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
|
|
2385
|
+
const referenced = extractClosesReferences(prBody);
|
|
2386
|
+
if (taskIssueRefs) {
|
|
2387
|
+
for (const n of referenced) {
|
|
2388
|
+
const ref = taskIssueRefs.get(n);
|
|
2389
|
+
if (!ref)
|
|
2390
|
+
continue;
|
|
2391
|
+
const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`;
|
|
2392
|
+
if (branch !== expectedBranch) {
|
|
2393
|
+
return {
|
|
2394
|
+
ok: false,
|
|
2395
|
+
message: `closes-n-reverse: branch "${branch}" closes #${n} (task ${ref.taskId} of tranche "${ref.trancheSlug}") but is not named "${expectedBranch}" — rename the branch and re-push, or if this work is intentionally outside AEG's dispatch flow, remove the Closes reference.`
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const m = branch.match(/^task\/([^/]+)\/([^/]+)$/);
|
|
2401
|
+
if (!m)
|
|
2402
|
+
return { ok: true };
|
|
2403
|
+
const trancheSlug = m[1];
|
|
2404
|
+
const taskId = m[2];
|
|
2405
|
+
const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug);
|
|
2406
|
+
if (!trancheFile) {
|
|
2407
|
+
return {
|
|
2408
|
+
ok: false,
|
|
2409
|
+
message: `closes-n: branch "${branch}" references tranche "${trancheSlug}" but no topology file found at aeg-root/tranches/${trancheSlug}.md. Ensure the tranche file exists before opening the PR.`
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
const task = trancheFile.tranche.tasks.find((t) => t.id === taskId);
|
|
2413
|
+
if (!task) {
|
|
2414
|
+
return {
|
|
2415
|
+
ok: false,
|
|
2416
|
+
message: `closes-n: branch "${branch}" references task "${taskId}" not found in ${trancheSlug} topology. Verify the task ID matches the tranche file.`
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
if (task.issue === null) {
|
|
2420
|
+
return {
|
|
2421
|
+
ok: false,
|
|
2422
|
+
message: `closes-n: task "${taskId}" in "${trancheSlug}" has no Issue number (#TBD). The Planner must cut the Issue before this PR can be validated.`
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
const expectedIssue = task.issue;
|
|
2426
|
+
if (!referenced.has(expectedIssue)) {
|
|
2427
|
+
return {
|
|
2428
|
+
ok: false,
|
|
2429
|
+
expectedIssue,
|
|
2430
|
+
message: `closes-n: PR body does not contain \`Closes #${expectedIssue}\` (required for task "${taskId}" in tranche "${trancheSlug}"). Add it to the PR body Summary section.`
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
return { ok: true, expectedIssue };
|
|
2434
|
+
}
|
|
2435
|
+
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
2436
|
+
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
2437
|
+
function extractHeadSha(comment) {
|
|
2438
|
+
const m = comment.match(HEAD_SHA_PATTERN);
|
|
2439
|
+
return m ? m[1].toLowerCase() : null;
|
|
2440
|
+
}
|
|
2441
|
+
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
2442
|
+
const clearHits = comments.filter((c) => valuePattern.test(c));
|
|
2443
|
+
if (clearHits.length > 0) {
|
|
2444
|
+
const latest = clearHits[clearHits.length - 1];
|
|
2445
|
+
const m = latest.match(valuePattern);
|
|
2446
|
+
return {
|
|
2447
|
+
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
2448
|
+
headSha: extractHeadSha(latest),
|
|
2449
|
+
danglingNote: null
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
return {
|
|
2453
|
+
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
2454
|
+
headSha: null,
|
|
2455
|
+
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function extractCodeReviewVerdict(comments) {
|
|
2459
|
+
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
|
|
2460
|
+
}
|
|
2461
|
+
function extractSecurityReviewVerdict(comments) {
|
|
2462
|
+
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
|
|
2463
|
+
}
|
|
2464
|
+
// ../../packages/aeg-core/src/review-gate.ts
|
|
2465
|
+
function isBoundToHead(extraction, headSha) {
|
|
2466
|
+
if (!extraction.headSha)
|
|
2467
|
+
return false;
|
|
2468
|
+
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
2469
|
+
}
|
|
2470
|
+
function checkReviewGate(input) {
|
|
2471
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2472
|
+
const waived = isWaiverLabelActorVerified({
|
|
2473
|
+
label: WAIVER_LABEL_REVIEW,
|
|
2474
|
+
labels: input.labels,
|
|
2475
|
+
labelActor: input.waiverLabelActor,
|
|
2476
|
+
principalAllowlist
|
|
2477
|
+
});
|
|
2478
|
+
if (waived) {
|
|
2479
|
+
return {
|
|
2480
|
+
verdict: "pass",
|
|
2481
|
+
reason: `\`${WAIVER_LABEL_REVIEW}\` label is actor-verified — review requirement waived for this PR.`,
|
|
2482
|
+
waived: true
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
const verified = input.comments.filter((c) => isPrincipal(c.author, principalAllowlist));
|
|
2486
|
+
const ignoredCount = input.comments.filter((c) => !isPrincipal(c.author, principalAllowlist) && c.body.includes("VERDICT")).length;
|
|
2487
|
+
const verifiedBodies = verified.map((c) => c.body);
|
|
2488
|
+
const codeReview = extractCodeReviewVerdict(verifiedBodies);
|
|
2489
|
+
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2490
|
+
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2491
|
+
const securityClean = security.value === "PASS";
|
|
2492
|
+
const codeReviewBound = isBoundToHead(codeReview, input.headSha);
|
|
2493
|
+
const securityBound = isBoundToHead(security, input.headSha);
|
|
2494
|
+
if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
|
|
2495
|
+
return {
|
|
2496
|
+
verdict: "pass",
|
|
2497
|
+
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
|
|
2498
|
+
waived: false
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
const problems = [];
|
|
2502
|
+
if (!codeReviewClean) {
|
|
2503
|
+
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2504
|
+
} else if (!codeReviewBound) {
|
|
2505
|
+
problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
2506
|
+
}
|
|
2507
|
+
if (!securityClean) {
|
|
2508
|
+
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2509
|
+
} else if (!securityBound) {
|
|
2510
|
+
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
2511
|
+
}
|
|
2512
|
+
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2513
|
+
return {
|
|
2514
|
+
verdict: "fail",
|
|
2515
|
+
reason: `${problems.join("; ")}. A principal can apply an actor-verified \`${WAIVER_LABEL_REVIEW}\` label to skip this requirement, or post the missing/clean verdict comment(s).${ignoredNote}`,
|
|
2516
|
+
waived: false
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
var CHANGESET_RELEASE_BRANCH = "changeset-release/main";
|
|
2520
|
+
var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
|
|
2521
|
+
function isChangesetsReleasePr(branch, author, expectedAuthor) {
|
|
2522
|
+
return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
|
|
2523
|
+
}
|
|
2524
|
+
// ../../packages/aeg-core/src/markdown-table.ts
|
|
2525
|
+
function splitRow(line) {
|
|
2526
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
2527
|
+
return trimmed.split("|").map((cell) => cell.trim());
|
|
2528
|
+
}
|
|
2529
|
+
var TABLE_ROW_PATTERN = /^\s*\|.*\|\s*$/;
|
|
2530
|
+
var SEPARATOR_ROW_PATTERN = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
|
|
2531
|
+
function findTable(lines, fromLine, stopAtHeading = true) {
|
|
2532
|
+
let i = fromLine - 1;
|
|
2533
|
+
while (i < lines.length) {
|
|
2534
|
+
const line = lines[i] ?? "";
|
|
2535
|
+
if (stopAtHeading && i > fromLine - 1 && /^##\s/.test(line))
|
|
2536
|
+
return null;
|
|
2537
|
+
if (TABLE_ROW_PATTERN.test(line)) {
|
|
2538
|
+
const headerLine = line;
|
|
2539
|
+
const sepLine = lines[i + 1] ?? "";
|
|
2540
|
+
if (!SEPARATOR_ROW_PATTERN.test(sepLine)) {
|
|
2541
|
+
i++;
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
const headers = splitRow(headerLine);
|
|
2545
|
+
const rows = [];
|
|
2546
|
+
let j = i + 2;
|
|
2547
|
+
while (j < lines.length && TABLE_ROW_PATTERN.test(lines[j] ?? "")) {
|
|
2548
|
+
rows.push({ cells: splitRow(lines[j] ?? ""), line: j + 1 });
|
|
2549
|
+
j++;
|
|
2550
|
+
}
|
|
2551
|
+
return { headers, rows };
|
|
2552
|
+
}
|
|
2553
|
+
i++;
|
|
2554
|
+
}
|
|
2555
|
+
return null;
|
|
2556
|
+
}
|
|
2557
|
+
function findHeadingLine(lines, pattern) {
|
|
2558
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2559
|
+
if (pattern.test(lines[i] ?? ""))
|
|
2560
|
+
return i + 1;
|
|
2561
|
+
}
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
// ../../packages/aeg-core/src/diagram-model.ts
|
|
2565
|
+
import matter from "gray-matter";
|
|
2566
|
+
|
|
2567
|
+
// ../../packages/aeg-core/src/registry-parse.ts
|
|
2568
|
+
var RING_HEADINGS = [
|
|
2569
|
+
{ ring: "ring0", pattern: /^##\s+Ring 0\b/ },
|
|
2570
|
+
{ ring: "ring1", pattern: /^##\s+Ring 1\b/ },
|
|
2571
|
+
{ ring: "ring2", pattern: /^##\s+Ring 2\b/ }
|
|
2572
|
+
];
|
|
2573
|
+
function stripBackticks2(cell) {
|
|
2574
|
+
const trimmed = cell.trim();
|
|
2575
|
+
if (trimmed.length >= 2 && trimmed.startsWith("`") && trimmed.endsWith("`")) {
|
|
2576
|
+
return trimmed.slice(1, -1);
|
|
2577
|
+
}
|
|
2578
|
+
return trimmed;
|
|
2579
|
+
}
|
|
2580
|
+
function stripBold(cell) {
|
|
2581
|
+
return cell.trim().replace(/\*\*/g, "");
|
|
2582
|
+
}
|
|
2583
|
+
function parseEnforcementRegistry(content) {
|
|
2584
|
+
const lines = content.split(`
|
|
2585
|
+
`);
|
|
2586
|
+
const result = [];
|
|
2587
|
+
for (const { ring, pattern } of RING_HEADINGS) {
|
|
2588
|
+
const headingLine = findHeadingLine(lines, pattern);
|
|
2589
|
+
if (headingLine === null)
|
|
2590
|
+
continue;
|
|
2591
|
+
const table = findTable(lines, headingLine + 1);
|
|
2592
|
+
if (!table)
|
|
2593
|
+
continue;
|
|
2594
|
+
const descriptionIndex = table.headers.findIndex((h) => h.trim().toLowerCase() === "description");
|
|
2595
|
+
const implementationIndex = table.headers.findIndex((h) => h.trim().toLowerCase() === "implementation");
|
|
2596
|
+
for (const row of table.rows) {
|
|
2597
|
+
const cells = row.cells;
|
|
2598
|
+
if (cells.length < 3)
|
|
2599
|
+
continue;
|
|
2600
|
+
const action = stripBold(cells[0] ?? "");
|
|
2601
|
+
const summary = stripBackticks2(cells[1] ?? "");
|
|
2602
|
+
const category = stripBackticks2(cells[2] ?? "");
|
|
2603
|
+
const implementation = stripBackticks2((implementationIndex === -1 ? cells[cells.length - 1] : cells[implementationIndex]) ?? "");
|
|
2604
|
+
const description = descriptionIndex === -1 ? undefined : stripBackticks2(cells[descriptionIndex] ?? "") || undefined;
|
|
2605
|
+
const spec = cells.length > 4 ? stripBackticks2(cells[cells.length - 2] ?? "") : undefined;
|
|
2606
|
+
result.push({ ring, action, summary, category, description, spec, implementation, line: row.line });
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
return result;
|
|
2610
|
+
}
|
|
2611
|
+
// ../../packages/aeg-core/src/registry-checks.ts
|
|
2612
|
+
function checkG1(rows, existsFn) {
|
|
2613
|
+
const findings = [];
|
|
2614
|
+
for (const row of rows) {
|
|
2615
|
+
if (row.implementation === "")
|
|
2616
|
+
continue;
|
|
2617
|
+
if (!existsFn(row.implementation)) {
|
|
2618
|
+
findings.push({
|
|
2619
|
+
row: row.action,
|
|
2620
|
+
path: row.implementation,
|
|
2621
|
+
reason: `${row.ring} row "${row.action}" names implementation "${row.implementation}", which does not exist on disk`
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return { check: "G1", status: findings.length > 0 ? "info" : "pass", findings };
|
|
2626
|
+
}
|
|
2627
|
+
function checkG2(rows, candidateFiles) {
|
|
2628
|
+
const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
|
|
2629
|
+
const findings = [];
|
|
2630
|
+
for (const path of candidateFiles) {
|
|
2631
|
+
if (!implementations.has(path)) {
|
|
2632
|
+
findings.push({
|
|
2633
|
+
path,
|
|
2634
|
+
reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
|
|
2639
|
+
}
|
|
2640
|
+
function checkG3(ring0Rows, crossingFiles) {
|
|
2641
|
+
const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
|
|
2642
|
+
const findings = [];
|
|
2643
|
+
for (const path of crossingFiles) {
|
|
2644
|
+
if (!ring0Implementations.has(path)) {
|
|
2645
|
+
findings.push({
|
|
2646
|
+
path,
|
|
2647
|
+
reason: `"${path}" makes a GitHub-crossing call but is not named by any Ring-0 row's implementation — a seventh, unlisted way into GitHub`
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return { check: "G3", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
2652
|
+
}
|
|
2653
|
+
function checkG4(content, resolveFn) {
|
|
2654
|
+
const cited = new Set;
|
|
2655
|
+
for (const hit of content.matchAll(/#(\d{3,})\b/g)) {
|
|
2656
|
+
cited.add(Number(hit[1]));
|
|
2657
|
+
}
|
|
2658
|
+
const findings = [];
|
|
2659
|
+
for (const n of cited) {
|
|
2660
|
+
if (!resolveFn(n)) {
|
|
2661
|
+
findings.push({
|
|
2662
|
+
reason: `#${n} is cited in enforcement.md but does not resolve to a real Issue or PR in the forge`
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return { check: "G4", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
2667
|
+
}
|
|
2668
|
+
function checkG5(roles, contracts) {
|
|
2669
|
+
const roleIds = new Set(roles.map((r) => r.role_id));
|
|
2670
|
+
const findings = [];
|
|
2671
|
+
for (const contract of contracts) {
|
|
2672
|
+
if (!roleIds.has(contract.producer)) {
|
|
2673
|
+
findings.push({
|
|
2674
|
+
path: contract.file,
|
|
2675
|
+
reason: `contract "${contract.file}" names producer "${contract.producer}", which is not a real role_id`
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
if (!roleIds.has(contract.consumer)) {
|
|
2679
|
+
findings.push({
|
|
2680
|
+
path: contract.file,
|
|
2681
|
+
reason: `contract "${contract.file}" names consumer "${contract.consumer}", which is not a real role_id`
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
for (const role of roles) {
|
|
2686
|
+
if (role.performs.length === 0) {
|
|
2687
|
+
findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty performs array` });
|
|
2688
|
+
}
|
|
2689
|
+
if (role.refuses_when.trim() === "") {
|
|
2690
|
+
findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty refuses_when` });
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
return { check: "G5", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
2694
|
+
}
|
|
2695
|
+
// ../../packages/aeg-core/src/dead-branch-push-guard.ts
|
|
2696
|
+
function checkDeadBranchPush(input) {
|
|
2697
|
+
const { branch, prState, prNumber } = input;
|
|
2698
|
+
if (prState === "MERGED") {
|
|
2699
|
+
return {
|
|
2700
|
+
verdict: "refuse",
|
|
2701
|
+
reason: `Branch \`${branch}\` already has a MERGED PR (#${prNumber}) — this branch's work is done. Push a new task branch instead of pushing onto an already-resolved one.`
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
if (prState === "CLOSED") {
|
|
2705
|
+
return {
|
|
2706
|
+
verdict: "refuse",
|
|
2707
|
+
reason: `Branch \`${branch}\` has a CLOSED PR (#${prNumber}) that was never merged — pushing here would resurrect abandoned/rejected work. Open a new task branch instead.`
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
if (prState === "OPEN") {
|
|
2711
|
+
return {
|
|
2712
|
+
verdict: "allow",
|
|
2713
|
+
reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
if (prState === "NONE") {
|
|
2717
|
+
return {
|
|
2718
|
+
verdict: "allow",
|
|
2719
|
+
reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
return {
|
|
2723
|
+
verdict: "allow",
|
|
2724
|
+
reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
2728
|
+
var SHIPS_PREFIX = "aeg-root/";
|
|
2729
|
+
var SHIPS_ARCHIVE_PREFIX = "aeg-root/tranches/completed/";
|
|
2730
|
+
function isSpecFile2(path) {
|
|
2731
|
+
return path.startsWith("apps/") && path.includes("/specs/") && path.endsWith(".md");
|
|
2732
|
+
}
|
|
2733
|
+
function isClaudeMdFile(path) {
|
|
2734
|
+
return path === "CLAUDE.md" || path.endsWith("/CLAUDE.md");
|
|
2735
|
+
}
|
|
2736
|
+
function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix) {
|
|
2737
|
+
if (path.startsWith(SHIPS_PREFIX)) {
|
|
2738
|
+
return path.startsWith(SHIPS_ARCHIVE_PREFIX) ? "internal" : "ships";
|
|
2739
|
+
}
|
|
2740
|
+
if (path.startsWith(readerFacingPrefix) && path.endsWith(readerFacingSuffix)) {
|
|
2741
|
+
return "reader-facing";
|
|
2742
|
+
}
|
|
2743
|
+
if (isSpecFile2(path) || isClaudeMdFile(path))
|
|
2744
|
+
return "internal";
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
2748
|
+
function stripNonProse(path, content) {
|
|
2749
|
+
if (path.endsWith(".md")) {
|
|
2750
|
+
return content.replace(/```.*?```/gs, (m) => m.replace(/[^\n]/g, "")).replace(/`[^`\n]*`/g, (m) => m.replace(/[^\n]/g, ""));
|
|
2751
|
+
}
|
|
2752
|
+
if (path.endsWith(".tsx") || path.endsWith(".ts")) {
|
|
2753
|
+
return content.replace(/\/\*.*?\*\//gs, (m) => m.replace(/[^\n]/g, "")).replace(/(^|[^:])\/\/[^\n]*/g, (m) => m.replace(/[^\n]/g, ""));
|
|
2754
|
+
}
|
|
2755
|
+
return content;
|
|
2756
|
+
}
|
|
2757
|
+
function lineAt(content, index) {
|
|
2758
|
+
let line = 1;
|
|
2759
|
+
for (let i = 0;i < index; i++) {
|
|
2760
|
+
if (content.charCodeAt(i) === 10)
|
|
2761
|
+
line++;
|
|
2762
|
+
}
|
|
2763
|
+
return line;
|
|
2764
|
+
}
|
|
2765
|
+
var FORGE_NUMBER_PATTERN = /#[0-9]{2,4}/g;
|
|
2766
|
+
var TRANCHE_SLUG_VN_PATTERN = /[a-z][a-z-]+-v[0-9]/g;
|
|
2767
|
+
function legacySlugPattern(legacySlugs) {
|
|
2768
|
+
if (legacySlugs.length === 0)
|
|
2769
|
+
return null;
|
|
2770
|
+
const alternation = legacySlugs.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
2771
|
+
return new RegExp(`(^|[^a-zA-Z0-9\\n-])(${alternation})([^a-zA-Z0-9\\n-]|$)`, "g");
|
|
2772
|
+
}
|
|
2773
|
+
function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
|
|
2774
|
+
const findings = [];
|
|
2775
|
+
const legacyPattern = legacySlugPattern(legacySlugs);
|
|
2776
|
+
const patterns = [
|
|
2777
|
+
{ pattern: FORGE_NUMBER_PATTERN, what: "a forge number" },
|
|
2778
|
+
{ pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug" },
|
|
2779
|
+
...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
|
|
2780
|
+
];
|
|
2781
|
+
for (const file of files) {
|
|
2782
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
|
|
2783
|
+
if (!cls || !SWEPT_CLASSES.has(cls))
|
|
2784
|
+
continue;
|
|
2785
|
+
const scrubbed = stripNonProse(file.path, file.content);
|
|
2786
|
+
for (const { pattern, what, group } of patterns) {
|
|
2787
|
+
pattern.lastIndex = 0;
|
|
2788
|
+
let match = pattern.exec(scrubbed);
|
|
2789
|
+
while (match !== null) {
|
|
2790
|
+
const cited = group !== undefined ? match[group] ?? match[0] : match[0];
|
|
2791
|
+
findings.push({
|
|
2792
|
+
file: file.path,
|
|
2793
|
+
line: lineAt(scrubbed, match.index),
|
|
2794
|
+
message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`
|
|
2795
|
+
});
|
|
2796
|
+
match = pattern.exec(scrubbed);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
return findings;
|
|
2801
|
+
}
|
|
2802
|
+
function escapeTerm(term) {
|
|
2803
|
+
return term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2804
|
+
}
|
|
2805
|
+
function containsWholeWord(text, term) {
|
|
2806
|
+
const escaped = escapeTerm(term);
|
|
2807
|
+
const re = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)`, "i");
|
|
2808
|
+
return re.test(text);
|
|
2809
|
+
}
|
|
2810
|
+
function findWholeWordLine(content, term) {
|
|
2811
|
+
const lines = content.split(`
|
|
2812
|
+
`);
|
|
2813
|
+
let lineNo = 0;
|
|
2814
|
+
for (const line of lines) {
|
|
2815
|
+
lineNo++;
|
|
2816
|
+
if (containsWholeWord(line, term))
|
|
2817
|
+
return lineNo;
|
|
2818
|
+
}
|
|
2819
|
+
return null;
|
|
2820
|
+
}
|
|
2821
|
+
function definesOrLinksGlossary(content, term) {
|
|
2822
|
+
const escaped = escapeTerm(term);
|
|
2823
|
+
const definesInline = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)[^\\n]{0,3}—`, "i").test(content);
|
|
2824
|
+
if (definesInline)
|
|
2825
|
+
return true;
|
|
2826
|
+
return /docs\/glossary|glossary\.md/i.test(content);
|
|
2827
|
+
}
|
|
2828
|
+
function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix) {
|
|
2829
|
+
const findings = [];
|
|
2830
|
+
for (const file of files) {
|
|
2831
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
|
|
2832
|
+
if (!cls || !SWEPT_CLASSES.has(cls))
|
|
2833
|
+
continue;
|
|
2834
|
+
const scrubbed = stripNonProse(file.path, file.content);
|
|
2835
|
+
for (const term of glossaryTerms) {
|
|
2836
|
+
const line = findWholeWordLine(scrubbed, term);
|
|
2837
|
+
if (line === null)
|
|
2838
|
+
continue;
|
|
2839
|
+
if (definesOrLinksGlossary(scrubbed, term))
|
|
2840
|
+
continue;
|
|
2841
|
+
findings.push({
|
|
2842
|
+
file: file.path,
|
|
2843
|
+
line,
|
|
2844
|
+
message: `uses coined term "${term}" without defining it inline or linking the glossary`
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
return findings;
|
|
2849
|
+
}
|
|
2850
|
+
function checkReaderResolvableProse(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
|
|
2851
|
+
return [
|
|
2852
|
+
...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs),
|
|
2853
|
+
...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix)
|
|
2854
|
+
];
|
|
2855
|
+
}
|
|
2856
|
+
function parseGlossaryTerms(glossaryContent) {
|
|
2857
|
+
const terms = [];
|
|
2858
|
+
const re = /^\*\*(.+?)\*\* —/gm;
|
|
2859
|
+
let match = re.exec(glossaryContent);
|
|
2860
|
+
while (match !== null) {
|
|
2861
|
+
if (match[1] !== undefined)
|
|
2862
|
+
terms.push(match[1]);
|
|
2863
|
+
match = re.exec(glossaryContent);
|
|
2864
|
+
}
|
|
2865
|
+
return terms;
|
|
2866
|
+
}
|
|
2867
|
+
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2868
|
+
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2869
|
+
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
2870
|
+
}
|
|
2871
|
+
function checkDispatchReadiness(input) {
|
|
2872
|
+
const { trancheSlug, task } = input;
|
|
2873
|
+
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
2874
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2875
|
+
const blockers = [];
|
|
2876
|
+
if (task.issue === null) {
|
|
2877
|
+
blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
|
|
2878
|
+
} else if (input.issue === null) {
|
|
2879
|
+
blockers.push(`dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
|
|
2880
|
+
}
|
|
2881
|
+
if (input.issue !== null && !input.issueRationalePass) {
|
|
2882
|
+
blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
|
|
2883
|
+
}
|
|
2884
|
+
for (const dep of input.dependsOn) {
|
|
2885
|
+
if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
|
|
2886
|
+
const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
|
|
2887
|
+
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
for (const c of input.conflictsWith) {
|
|
2891
|
+
if (c.openOrInFlight) {
|
|
2892
|
+
const issueStr = c.issue !== null ? ` (#${c.issue})` : "";
|
|
2893
|
+
blockers.push(`dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
for (const proj of input.priorTrancheArchival) {
|
|
2897
|
+
if (proj.priorTrancheSlug !== null && !proj.archived) {
|
|
2898
|
+
blockers.push(`dispatch-gate prior-tranche-archival: project \`${proj.project}\`'s previous tranche \`${proj.priorTrancheSlug}\` is not archived — the Tranche Archivist must run before new work on this product.`);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
return { ready: blockers.length === 0, blockers };
|
|
2902
|
+
}
|
|
2903
|
+
// ../../packages/aeg-core/src/single-plan-pr.ts
|
|
2904
|
+
function trancheSlugFromTopologyPath(path) {
|
|
2905
|
+
const m = path.match(/^aeg-root\/tranches\/([^/]+)\.md$/);
|
|
2906
|
+
if (!m)
|
|
2907
|
+
return null;
|
|
2908
|
+
const slug = m[1];
|
|
2909
|
+
if (slug === "README" || slug.endsWith(".tokens"))
|
|
2910
|
+
return null;
|
|
2911
|
+
return slug;
|
|
2912
|
+
}
|
|
2913
|
+
function checkSinglePlanPr(branchFiles, otherOpenPrs) {
|
|
2914
|
+
const touchedSlugs = new Set(branchFiles.map(trancheSlugFromTopologyPath).filter((s) => s !== null));
|
|
2915
|
+
if (touchedSlugs.size === 0)
|
|
2916
|
+
return { ok: true };
|
|
2917
|
+
for (const pr of otherOpenPrs) {
|
|
2918
|
+
const otherSlugs = new Set(pr.files.map(trancheSlugFromTopologyPath).filter((s) => s !== null));
|
|
2919
|
+
for (const slug of touchedSlugs) {
|
|
2920
|
+
if (otherSlugs.has(slug)) {
|
|
2921
|
+
return {
|
|
2922
|
+
ok: false,
|
|
2923
|
+
message: `single-plan-pr: another open PR (#${pr.number}) already touches tranche "${slug}"'s topology file. Only one open plan PR per tranche is allowed at a time — wait for #${pr.number} to merge or close, or coordinate with its author.`
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return { ok: true };
|
|
2929
|
+
}
|
|
2930
|
+
function touchesAnyTopology(files) {
|
|
2931
|
+
return files.some((f) => trancheSlugFromTopologyPath(f) !== null);
|
|
2932
|
+
}
|
|
2933
|
+
// ../../packages/aeg-core/src/no-disk-state.ts
|
|
2934
|
+
var TOP_LEVEL_TOPOLOGY_FILE = /^aeg-root\/tranches\/[^/]+\.md$/;
|
|
2935
|
+
var ANY_DEPTH_TRANCHES_MD = /^aeg-root\/tranches\/.*\.md$/;
|
|
2936
|
+
var TOKENS_FILE = /\.tokens\.md$/;
|
|
2937
|
+
function isNewDiskStateFile(path, status) {
|
|
2938
|
+
if (TOP_LEVEL_TOPOLOGY_FILE.test(path))
|
|
2939
|
+
return true;
|
|
2940
|
+
if (status !== "added")
|
|
2941
|
+
return false;
|
|
2942
|
+
return ANY_DEPTH_TRANCHES_MD.test(path) || TOKENS_FILE.test(path);
|
|
2943
|
+
}
|
|
2944
|
+
// ../../packages/aeg-core/src/branch-topology-gate.ts
|
|
2945
|
+
function taskBranchTopologyFields(branch) {
|
|
2946
|
+
if (!/^task\/.*\/.*$/.test(branch))
|
|
2947
|
+
return null;
|
|
2948
|
+
const parts = branch.split("/");
|
|
2949
|
+
return { tranche: parts[1] ?? "", taskId: parts[2] ?? "" };
|
|
2950
|
+
}
|
|
2951
|
+
function checkBranchTopology(input) {
|
|
2952
|
+
const { branch, tranche, taskId, topoPath, topology } = input;
|
|
2953
|
+
if (topology === null) {
|
|
2954
|
+
return {
|
|
2955
|
+
verdict: "refuse",
|
|
2956
|
+
reason: `✖ pre-push: branch \`${branch}\` names tranche \`${tranche}\`, but ${topoPath} does not exist.
|
|
2957
|
+
` + " A task branch must belong to a real tranche."
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
const hasRow = topology?.tasks.some((t) => t.id === taskId) ?? false;
|
|
2961
|
+
if (!hasRow) {
|
|
2962
|
+
return {
|
|
2963
|
+
verdict: "refuse",
|
|
2964
|
+
reason: `✖ pre-push: branch \`${branch}\` — no row with \`#\` = \`${taskId}\` in ${topoPath}.
|
|
2965
|
+
` + ` The branch suffix must literal-match the topology's # column.
|
|
2966
|
+
` + " If the plan PR adding this row hasn't merged yet, merge it first."
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
return {
|
|
2970
|
+
verdict: "allow",
|
|
2971
|
+
reason: `Branch \`${branch}\` matches topology row \`${taskId}\` in ${topoPath}.`
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
// ../../packages/aeg-core/src/first-push-dispatch-gate.ts
|
|
2975
|
+
function parseTaskBranch(branch) {
|
|
2976
|
+
const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch);
|
|
2977
|
+
return m ? { tranche: m[1], taskId: m[2] } : null;
|
|
2978
|
+
}
|
|
2979
|
+
function checkFirstPushDispatchGate(input) {
|
|
2980
|
+
const { branch, prExists, readiness } = input;
|
|
2981
|
+
if (parseTaskBranch(branch) === null) {
|
|
2982
|
+
return {
|
|
2983
|
+
verdict: "allow",
|
|
2984
|
+
reason: `Branch \`${branch}\` is not a task/<tranche>/<n> branch — the first-push dispatch gate only applies to task branches.`
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
if (prExists) {
|
|
2988
|
+
return {
|
|
2989
|
+
verdict: "allow",
|
|
2990
|
+
reason: `Branch \`${branch}\` already has an open PR — dispatch readiness was already validated on its first push; later pushes are not re-blocked by a sibling task's state change.`
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
if (readiness === "UNKNOWN") {
|
|
2994
|
+
return {
|
|
2995
|
+
verdict: "allow",
|
|
2996
|
+
reason: `verify-dispatch could not reach the forge for \`${branch}\` (repo/token unresolvable) — failing OPEN rather than blocking the push on a transient issue.`
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
if (readiness === "NOT_READY") {
|
|
3000
|
+
return {
|
|
3001
|
+
verdict: "refuse",
|
|
3002
|
+
reason: `verify-dispatch reports NOT READY for \`${branch}\` — see the failing predicate printed above.`
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
return {
|
|
3006
|
+
verdict: "allow",
|
|
3007
|
+
reason: `verify-dispatch reports READY TO DISPATCH for \`${branch}\`.`
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
// ../../packages/aeg-core/src/issue-assignment.ts
|
|
3011
|
+
function decideIssueAssignment(input) {
|
|
3012
|
+
const { branch, remoteRefExists, issue, assignees, login } = input;
|
|
3013
|
+
if (parseTaskBranch(branch) === null) {
|
|
3014
|
+
return {
|
|
3015
|
+
action: "skip",
|
|
3016
|
+
reason: `Branch \`${branch}\` is not a task/<tranche>/<n> branch — Issue self-assignment only applies to task branches.`
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
if (remoteRefExists) {
|
|
3020
|
+
return {
|
|
3021
|
+
action: "skip",
|
|
3022
|
+
reason: `Branch \`${branch}\` already exists on the remote — not its first push, nothing to assign.`
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
if (issue === null) {
|
|
3026
|
+
return {
|
|
3027
|
+
action: "skip",
|
|
3028
|
+
reason: `No Issue resolved for \`${branch}\` from its topology row — skipping self-assignment (the gate owns refusing an unplanned branch).`
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
if (assignees === null) {
|
|
3032
|
+
return {
|
|
3033
|
+
action: "skip",
|
|
3034
|
+
reason: `Could not read Issue #${issue}'s current assignees — skipping rather than risking a double-assign (fail-open, never a guess).`
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
if (assignees.length > 0) {
|
|
3038
|
+
return {
|
|
3039
|
+
action: "skip",
|
|
3040
|
+
reason: `Issue #${issue} is already assigned (${assignees.join(", ")}) — no-op.`
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
if (login === null) {
|
|
3044
|
+
return {
|
|
3045
|
+
action: "skip",
|
|
3046
|
+
reason: "Could not resolve the authenticated `gh` login — never assigning anyone other than the actual pusher, so skipping."
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
return {
|
|
3050
|
+
action: "assign",
|
|
3051
|
+
issue,
|
|
3052
|
+
login,
|
|
3053
|
+
reason: `First push of \`${branch}\` — assigning Issue #${issue} to @${login} (the authenticated pusher).`
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
// ../../packages/aeg-core/src/test-plan-gate.ts
|
|
3057
|
+
var TASK_BRANCH_PATTERN2 = /^task\/[^/]+\/[^/]+$/;
|
|
3058
|
+
function evaluateTestPlanGate(body, branch) {
|
|
3059
|
+
if (!body) {
|
|
3060
|
+
return {
|
|
3061
|
+
verdict: "pass",
|
|
3062
|
+
messages: [
|
|
3063
|
+
"PR_BODY env var is empty; nothing to check.",
|
|
3064
|
+
"PASS (no body — likely a local invocation; CI sets PR_BODY automatically)."
|
|
3065
|
+
]
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
const located = locateTestPlanSection(body);
|
|
3069
|
+
if (!located.found) {
|
|
3070
|
+
if (TASK_BRANCH_PATTERN2.test(branch)) {
|
|
3071
|
+
return {
|
|
3072
|
+
verdict: "fail",
|
|
3073
|
+
messages: [
|
|
3074
|
+
`FAIL — no Test Plan section found in the PR body for task branch \`${branch}\`.`,
|
|
3075
|
+
"Searched for a `## Test Plan` / `## N. Test Plan` heading and an inline `**Test Plan:**`/`Test Plan:` marker — neither was found.",
|
|
3076
|
+
"Per, a task PR without a Test Plan is malformed, not exempt — add one (or `Test Plan: unit-tests-only` for a pure-logic brief with no runtime surface)."
|
|
3077
|
+
]
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
return {
|
|
3081
|
+
verdict: "pass",
|
|
3082
|
+
messages: [
|
|
3083
|
+
"No Test Plan section in PR body.",
|
|
3084
|
+
"PASS (advisory) — see aeg-root/roles/developer.md (Verification) and aeg-root/skills/brief-authoring/SKILL.md §9.",
|
|
3085
|
+
"Non-task branch (or BRANCH unset) — a brief that touches a runtime surface and has no Test Plan is malformed on a task branch; Brief Validation catches that case pre-dispatch."
|
|
3086
|
+
]
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
const section = located.section;
|
|
3090
|
+
if (/unit-tests-only/i.test(section)) {
|
|
3091
|
+
return {
|
|
3092
|
+
verdict: "pass",
|
|
3093
|
+
messages: ["Test Plan: unit-tests-only — pure-logic brief, CI unit-tests are the proof.", "PASS."]
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
const checkboxLines = section.split(`
|
|
3097
|
+
`).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]/.test(line));
|
|
3098
|
+
if (checkboxLines.length === 0) {
|
|
3099
|
+
return {
|
|
3100
|
+
verdict: "pass",
|
|
3101
|
+
messages: [
|
|
3102
|
+
"Test Plan section found but has no checkbox items.",
|
|
3103
|
+
"PASS (advisory) — the section may be empty or expressed in prose; verification is the Principal's call."
|
|
3104
|
+
]
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
const unchecked = checkboxLines.filter((line) => /^[-*]\s+\[\s\]/.test(line));
|
|
3108
|
+
const checked = checkboxLines.filter((line) => /^[-*]\s+\[[xX]\]/.test(line));
|
|
3109
|
+
const messages = [`Test Plan items: ${checked.length} ticked, ${unchecked.length} unticked.`];
|
|
3110
|
+
if (unchecked.length > 0) {
|
|
3111
|
+
messages.push("", "FAIL — the following Test Plan items are unticked:");
|
|
3112
|
+
for (const line of unchecked)
|
|
3113
|
+
messages.push(` ${line}`);
|
|
3114
|
+
messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any Test Plan box is unticked.", "- [agent] items: the Developer-agent posts the actual command output as evidence and ticks the box.", "- [principal] items: the Principal runs the item in a real signed-in browser and ticks the box.", "Note: editing the PR body does not retrigger most workflows. If your PR body changes do not surface here, push an empty commit to re-run.");
|
|
3115
|
+
return { verdict: "fail", messages };
|
|
3116
|
+
}
|
|
3117
|
+
messages.push("All Test Plan items ticked — runtime verification complete.", "PASS.");
|
|
3118
|
+
return { verdict: "pass", messages };
|
|
3119
|
+
}
|
|
3120
|
+
// src/lib/config.ts
|
|
3121
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3122
|
+
import { homedir } from "node:os";
|
|
3123
|
+
import { dirname, join } from "node:path";
|
|
3124
|
+
import { z } from "zod";
|
|
3125
|
+
var EnvEntrySchema = z.union([
|
|
3126
|
+
z.literal(true),
|
|
3127
|
+
z.object({ optional: z.literal(true) }),
|
|
3128
|
+
z.object({ anyOf: z.array(z.string()).min(2) }),
|
|
3129
|
+
z.string()
|
|
3130
|
+
]);
|
|
3131
|
+
var CheckEntrySchema = z.object({
|
|
3132
|
+
run: z.string(),
|
|
3133
|
+
scope: z.enum(["diff", "full"]),
|
|
3134
|
+
include: z.array(z.string()).optional(),
|
|
3135
|
+
args: z.array(z.string()).optional(),
|
|
3136
|
+
timeoutMs: z.number().optional(),
|
|
3137
|
+
env: z.record(z.string(), EnvEntrySchema).optional(),
|
|
3138
|
+
requiresOpenPr: z.boolean().optional(),
|
|
3139
|
+
ownWorkflow: z.boolean().optional()
|
|
3140
|
+
}).superRefine((entry2, ctx) => {
|
|
3141
|
+
if (!entry2.env)
|
|
3142
|
+
return;
|
|
3143
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
3144
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
3145
|
+
continue;
|
|
3146
|
+
const members = value.anyOf;
|
|
3147
|
+
if (new Set(members).size !== members.length) {
|
|
3148
|
+
ctx.addIssue({
|
|
3149
|
+
code: z.ZodIssueCode.custom,
|
|
3150
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
3151
|
+
path: ["env", key, "anyOf"]
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
if (!members.includes(key)) {
|
|
3155
|
+
ctx.addIssue({
|
|
3156
|
+
code: z.ZodIssueCode.custom,
|
|
3157
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
3158
|
+
path: ["env", key, "anyOf"]
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
});
|
|
3163
|
+
var BRIEF_BUILTINS = [
|
|
3164
|
+
"tier",
|
|
3165
|
+
"testPlan",
|
|
3166
|
+
"testPlanExclusivity",
|
|
3167
|
+
"principalPlaceholder",
|
|
3168
|
+
"surfaceMap",
|
|
3169
|
+
"docUpdateList",
|
|
3170
|
+
"worktreeStep0",
|
|
3171
|
+
"stopConditions",
|
|
3172
|
+
"autonomyClause",
|
|
3173
|
+
"project",
|
|
3174
|
+
"for",
|
|
3175
|
+
"closesN",
|
|
3176
|
+
"premiseCoverage",
|
|
3177
|
+
"issueRationale"
|
|
3178
|
+
];
|
|
3179
|
+
var BriefSectionSchema = z.union([
|
|
3180
|
+
z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
|
|
3181
|
+
z.object({ heading: z.string().min(1), name: z.string().optional() }),
|
|
3182
|
+
z.object({ field: z.string().min(1), name: z.string().optional() }),
|
|
3183
|
+
z.object({ phrase: z.string().min(1), name: z.string().optional() })
|
|
3184
|
+
]);
|
|
3185
|
+
var BriefSchemaSchema = z.object({
|
|
3186
|
+
pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
3187
|
+
issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
|
|
3188
|
+
});
|
|
3189
|
+
function isSafeRepoRelPath(p) {
|
|
3190
|
+
if (p.length === 0)
|
|
3191
|
+
return false;
|
|
3192
|
+
if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
|
|
3193
|
+
return false;
|
|
3194
|
+
return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
|
|
3195
|
+
}
|
|
3196
|
+
var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
|
|
3197
|
+
message: "must be a repo-root-relative path with no `..` segment or absolute root"
|
|
3198
|
+
});
|
|
3199
|
+
var ManagedBlockRecordSchema = z.object({
|
|
3200
|
+
path: SafeRepoRelPath,
|
|
3201
|
+
marker: z.string(),
|
|
3202
|
+
comment: z.enum(["hash", "html"])
|
|
3203
|
+
});
|
|
3204
|
+
var ManagedManifestSchema = z.object({
|
|
3205
|
+
version: z.number().int().positive(),
|
|
3206
|
+
files: z.array(SafeRepoRelPath),
|
|
3207
|
+
blocks: z.array(ManagedBlockRecordSchema),
|
|
3208
|
+
labels: z.array(z.string())
|
|
3209
|
+
});
|
|
3210
|
+
var VinayaConfigSchema = z.object({
|
|
3211
|
+
rings: z.object({
|
|
3212
|
+
ring1_forgeWriteInterception: z.boolean(),
|
|
3213
|
+
ring2_asyncAudits: z.boolean()
|
|
3214
|
+
}).optional(),
|
|
3215
|
+
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
3216
|
+
briefSchema: BriefSchemaSchema.optional(),
|
|
3217
|
+
managed: ManagedManifestSchema.optional(),
|
|
3218
|
+
principals: z.array(z.string()).min(1).optional(),
|
|
3219
|
+
releaseActor: z.string().min(1).optional(),
|
|
3220
|
+
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
3221
|
+
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
|
|
3222
|
+
});
|
|
3223
|
+
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
3224
|
+
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
3225
|
+
var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
|
|
3226
|
+
function resolvePrincipalAllowlist(config) {
|
|
3227
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
3228
|
+
}
|
|
3229
|
+
function resolveReleaseActor(config) {
|
|
3230
|
+
return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
|
|
3231
|
+
}
|
|
3232
|
+
function trustAnchorRepo() {
|
|
3233
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
3234
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
3235
|
+
if (fromRunner) {
|
|
3236
|
+
const validated = wellFormed(fromRunner);
|
|
3237
|
+
if (validated)
|
|
3238
|
+
return validated;
|
|
3239
|
+
}
|
|
3240
|
+
try {
|
|
3241
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
3242
|
+
encoding: "utf-8",
|
|
3243
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3244
|
+
}).trim();
|
|
3245
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
3246
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
3247
|
+
} catch {
|
|
3248
|
+
return null;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
function ghFetchTrustAnchorConfig() {
|
|
3252
|
+
const repo = trustAnchorRepo();
|
|
3253
|
+
if (!repo)
|
|
3254
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
3255
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
3256
|
+
encoding: "utf-8",
|
|
3257
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
function firstLine(err) {
|
|
3261
|
+
return err?.message?.split(`
|
|
3262
|
+
`)[0] ?? "unknown error";
|
|
3263
|
+
}
|
|
3264
|
+
function isMissingFileError(err) {
|
|
3265
|
+
const stderr = err?.stderr;
|
|
3266
|
+
const haystack = [
|
|
3267
|
+
err?.message ?? "",
|
|
3268
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
3269
|
+
].join(`
|
|
3270
|
+
`);
|
|
3271
|
+
return /\b404\b|not found/i.test(haystack);
|
|
3272
|
+
}
|
|
3273
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
3274
|
+
const warn = (why) => process.stdout.write(`⚠ could not read the trust-anchor config (\`principals\`/\`releaseActor\`) from the default branch (${why}) — falling back to vinaya's built-in defaults.
|
|
3275
|
+
`);
|
|
3276
|
+
let base64;
|
|
3277
|
+
try {
|
|
3278
|
+
base64 = fetcher().trim();
|
|
3279
|
+
} catch (err) {
|
|
3280
|
+
if (!isMissingFileError(err))
|
|
3281
|
+
warn(firstLine(err));
|
|
3282
|
+
return null;
|
|
3283
|
+
}
|
|
3284
|
+
if (!base64)
|
|
3285
|
+
return null;
|
|
3286
|
+
try {
|
|
3287
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
3288
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
3289
|
+
if (parsed.success)
|
|
3290
|
+
return parsed.data;
|
|
3291
|
+
warn("it did not satisfy the config schema");
|
|
3292
|
+
return null;
|
|
3293
|
+
} catch {
|
|
3294
|
+
warn("it was not readable as JSON");
|
|
3295
|
+
return null;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// src/checks/contract.ts
|
|
3300
|
+
var CHECK_SCHEMA_VERSION = 1;
|
|
3301
|
+
function emitCheckError(error) {
|
|
3302
|
+
process.stderr.write(`${JSON.stringify(error)}
|
|
3303
|
+
`);
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// src/checks/body-bare-digits-logic.ts
|
|
3307
|
+
function blankRange(text, start, end) {
|
|
3308
|
+
const region = text.slice(start, end);
|
|
3309
|
+
return text.slice(0, start) + region.replace(/[^\n]/g, " ") + text.slice(end);
|
|
3310
|
+
}
|
|
3311
|
+
var EXEMPT_ANCHOR_FIELDS = ["CLOSES", "TIER", "PROJECT", "EVIDENCE"];
|
|
3312
|
+
var FIELD_SECTION_HEADING = {
|
|
3313
|
+
CLOSES: null,
|
|
3314
|
+
PROJECT: null,
|
|
3315
|
+
TIER: /^scope$/i,
|
|
3316
|
+
EVIDENCE: /^evidence$/i
|
|
3317
|
+
};
|
|
3318
|
+
var HEADING = /^#{1,6}\s/;
|
|
3319
|
+
var HEADING_TEXT = /^#{1,6}\s+(.*?)\s*$/;
|
|
3320
|
+
function headerBlockEnd(body) {
|
|
3321
|
+
const lines = body.split(`
|
|
3322
|
+
`);
|
|
3323
|
+
let offset = 0;
|
|
3324
|
+
for (const line of lines) {
|
|
3325
|
+
if (HEADING.test(line))
|
|
3326
|
+
return offset;
|
|
3327
|
+
offset += line.length + 1;
|
|
3328
|
+
}
|
|
3329
|
+
return body.length;
|
|
3330
|
+
}
|
|
3331
|
+
function namedSectionBounds(body, namePattern) {
|
|
3332
|
+
const lines = body.split(`
|
|
3333
|
+
`);
|
|
3334
|
+
let offset = 0;
|
|
3335
|
+
let start = -1;
|
|
3336
|
+
let end = body.length;
|
|
3337
|
+
for (const line of lines) {
|
|
3338
|
+
const headingText = HEADING_TEXT.exec(line);
|
|
3339
|
+
if (start === -1 && headingText && namePattern.test(headingText[1])) {
|
|
3340
|
+
start = offset;
|
|
3341
|
+
} else if (start !== -1 && HEADING.test(line)) {
|
|
3342
|
+
end = offset;
|
|
3343
|
+
break;
|
|
3344
|
+
}
|
|
3345
|
+
offset += line.length + 1;
|
|
3346
|
+
}
|
|
3347
|
+
return start === -1 ? null : { start, end };
|
|
3348
|
+
}
|
|
3349
|
+
function isInCanonicalSection(body, field, outerStart) {
|
|
3350
|
+
const pattern = FIELD_SECTION_HEADING[field];
|
|
3351
|
+
if (pattern === null)
|
|
3352
|
+
return outerStart < headerBlockEnd(body);
|
|
3353
|
+
const bounds = namedSectionBounds(body, pattern);
|
|
3354
|
+
return bounds !== null && outerStart >= bounds.start && outerStart < bounds.end;
|
|
3355
|
+
}
|
|
3356
|
+
var PROJECT_LABEL = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?/i;
|
|
3357
|
+
var FIELD_CONTENT_SIGNATURE = {
|
|
3358
|
+
CLOSES: /Closes\s*#\d+/i,
|
|
3359
|
+
PROJECT: PROJECT_LABEL,
|
|
3360
|
+
TIER: TIER_FIELD,
|
|
3361
|
+
EVIDENCE: /^Head:\s*\S/m
|
|
3362
|
+
};
|
|
3363
|
+
function blankAnchoredRegions(body) {
|
|
3364
|
+
let masked = body;
|
|
3365
|
+
for (const field of EXEMPT_ANCHOR_FIELDS) {
|
|
3366
|
+
const bounds = anchoredRegionBounds(body, field);
|
|
3367
|
+
if (!bounds || !isInCanonicalSection(body, field, bounds.outerStart))
|
|
3368
|
+
continue;
|
|
3369
|
+
const content = body.slice(bounds.innerStart, bounds.innerEnd);
|
|
3370
|
+
if (!FIELD_CONTENT_SIGNATURE[field].test(content))
|
|
3371
|
+
continue;
|
|
3372
|
+
masked = blankRange(masked, bounds.outerStart, bounds.innerStart);
|
|
3373
|
+
masked = blankRange(masked, bounds.innerEnd, bounds.outerEnd);
|
|
3374
|
+
const blankedContent = content.split(`
|
|
3375
|
+
`).map(BOUNDED_ANCHOR_BLANK[field]).join(`
|
|
3376
|
+
`);
|
|
3377
|
+
masked = masked.slice(0, bounds.innerStart) + blankedContent + masked.slice(bounds.innerEnd);
|
|
3378
|
+
}
|
|
3379
|
+
return masked;
|
|
3380
|
+
}
|
|
3381
|
+
function blankTokenReportSection(body) {
|
|
3382
|
+
const lines = body.split(`
|
|
3383
|
+
`);
|
|
3384
|
+
const HEADING2 = /^#{1,6}\s/;
|
|
3385
|
+
const TOKEN_REPORT_HEADING = /^#{1,6}\s*token report\s*$/i;
|
|
3386
|
+
const startIdx = lines.findIndex((l) => TOKEN_REPORT_HEADING.test(l));
|
|
3387
|
+
if (startIdx === -1)
|
|
3388
|
+
return body;
|
|
3389
|
+
let endIdx = lines.length;
|
|
3390
|
+
for (let i = startIdx + 1;i < lines.length; i++) {
|
|
3391
|
+
if (HEADING2.test(lines[i])) {
|
|
3392
|
+
endIdx = i;
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
const fill = (line) => " ".repeat(line.length);
|
|
3397
|
+
for (let i = startIdx;i < endIdx; i++)
|
|
3398
|
+
lines[i] = fill(lines[i]);
|
|
3399
|
+
return lines.join(`
|
|
3400
|
+
`);
|
|
3401
|
+
}
|
|
3402
|
+
function blankTierField(line) {
|
|
3403
|
+
const m = TIER_FIELD.exec(line);
|
|
3404
|
+
if (!m)
|
|
3405
|
+
return line;
|
|
3406
|
+
return line.slice(0, m.index) + " ".repeat(m[0].length) + line.slice(m.index + m[0].length);
|
|
3407
|
+
}
|
|
3408
|
+
var SEGMENT_HAS_DIGIT = /\p{Nd}/u;
|
|
3409
|
+
function blankProjectField(line) {
|
|
3410
|
+
const label2 = PROJECT_LABEL.exec(line);
|
|
3411
|
+
if (!label2)
|
|
3412
|
+
return line;
|
|
3413
|
+
const valueStart = label2.index + label2[0].length;
|
|
3414
|
+
const segments = line.slice(valueStart).split(",");
|
|
3415
|
+
const blanked = segments.map((seg) => {
|
|
3416
|
+
const name = unwrapValue(seg);
|
|
3417
|
+
const isRealName = PROJECT_SLUG.test(name) && !SEGMENT_HAS_DIGIT.test(name);
|
|
3418
|
+
return isRealName ? " ".repeat(seg.length) : seg;
|
|
3419
|
+
});
|
|
3420
|
+
return line.slice(0, valueStart) + blanked.join(",");
|
|
3421
|
+
}
|
|
3422
|
+
function blankUnanchoredStructuralFields(body) {
|
|
3423
|
+
return body.split(`
|
|
3424
|
+
`).map((l) => blankProjectField(blankTierField(l))).join(`
|
|
3425
|
+
`);
|
|
3426
|
+
}
|
|
3427
|
+
var CLOSES_REF = /Closes\s*#\d+/i;
|
|
3428
|
+
var EVIDENCE_HEADING = /^#{1,6}\s/;
|
|
3429
|
+
var EVIDENCE_HEAD_LINE = /^Head:\s*\S+$/i;
|
|
3430
|
+
function blankClosesField(line) {
|
|
3431
|
+
const m = CLOSES_REF.exec(line);
|
|
3432
|
+
if (!m)
|
|
3433
|
+
return line;
|
|
3434
|
+
return line.slice(0, m.index) + " ".repeat(m[0].length) + line.slice(m.index + m[0].length);
|
|
3435
|
+
}
|
|
3436
|
+
function blankEvidenceField(line) {
|
|
3437
|
+
const trimmed = line.trim();
|
|
3438
|
+
if (trimmed === "" || EVIDENCE_HEAD_LINE.test(trimmed) || EVIDENCE_HEADING.test(trimmed))
|
|
3439
|
+
return " ".repeat(line.length);
|
|
3440
|
+
return line;
|
|
3441
|
+
}
|
|
3442
|
+
var BOUNDED_ANCHOR_BLANK = {
|
|
3443
|
+
CLOSES: blankClosesField,
|
|
3444
|
+
TIER: blankTierField,
|
|
3445
|
+
PROJECT: blankProjectField,
|
|
3446
|
+
EVIDENCE: blankEvidenceField
|
|
3447
|
+
};
|
|
3448
|
+
function buildScanMask(body) {
|
|
3449
|
+
let masked = maskDetailsBlocks(maskCode(body));
|
|
3450
|
+
masked = blankAnchoredRegions(masked);
|
|
3451
|
+
masked = blankTokenReportSection(masked);
|
|
3452
|
+
masked = blankUnanchoredStructuralFields(masked);
|
|
3453
|
+
return masked;
|
|
3454
|
+
}
|
|
3455
|
+
var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
|
|
3456
|
+
var LIST_MARKER_TOKEN = /^\p{Nd}{1,9}[.)]$/u;
|
|
3457
|
+
var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
3458
|
+
var HTML_ENTITIES = {
|
|
3459
|
+
""": '"',
|
|
3460
|
+
"'": "'",
|
|
3461
|
+
"<": "<",
|
|
3462
|
+
">": ">",
|
|
3463
|
+
"&": "&"
|
|
3464
|
+
};
|
|
3465
|
+
function decodeNamedEntities(body) {
|
|
3466
|
+
return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
|
|
3467
|
+
}
|
|
3468
|
+
function isLineLeadingListMarker(line, matchStart, rawToken) {
|
|
3469
|
+
if (!LIST_MARKER_TOKEN.test(rawToken))
|
|
3470
|
+
return false;
|
|
3471
|
+
const before = line.slice(0, matchStart);
|
|
3472
|
+
return /^ {0,3}$/.test(before);
|
|
3473
|
+
}
|
|
3474
|
+
function checkBareDigits(rawBody) {
|
|
3475
|
+
const body = decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
|
|
3476
|
+
const masked = buildScanMask(body);
|
|
3477
|
+
const maskedLines = masked.split(`
|
|
3478
|
+
`);
|
|
3479
|
+
const origLines = body.split(`
|
|
3480
|
+
`);
|
|
3481
|
+
const violations = [];
|
|
3482
|
+
for (let i = 0;i < maskedLines.length; i++) {
|
|
3483
|
+
const maskedLine = maskedLines[i];
|
|
3484
|
+
const origLine = origLines[i];
|
|
3485
|
+
TOKEN_WITH_DIGIT.lastIndex = 0;
|
|
3486
|
+
let m;
|
|
3487
|
+
while ((m = TOKEN_WITH_DIGIT.exec(maskedLine)) !== null) {
|
|
3488
|
+
const rawToken = origLine.slice(m.index, m.index + m[0].length);
|
|
3489
|
+
if (isLineLeadingListMarker(maskedLine, m.index, rawToken))
|
|
3490
|
+
continue;
|
|
3491
|
+
violations.push({ line: i + 1, text: origLine.trim() });
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
return { violations };
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// src/checks/bin/check-body-bare-digits.ts
|
|
3498
|
+
var CHECK_NAME = "body-bare-digits";
|
|
3499
|
+
function fetchPr(prNumber) {
|
|
3500
|
+
try {
|
|
3501
|
+
const out = execFileSync3("gh", ["pr", "view", String(prNumber), "--json", "number,headRefName,author"], {
|
|
3502
|
+
encoding: "utf8",
|
|
3503
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3504
|
+
});
|
|
3505
|
+
return JSON.parse(out);
|
|
3506
|
+
} catch {
|
|
3507
|
+
return null;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
function isExemptChangesetsReleasePr(prNumberStr) {
|
|
3511
|
+
if (!prNumberStr)
|
|
3512
|
+
return false;
|
|
3513
|
+
const prNumber = Number(prNumberStr);
|
|
3514
|
+
if (!Number.isFinite(prNumber))
|
|
3515
|
+
return false;
|
|
3516
|
+
const pr = fetchPr(prNumber);
|
|
3517
|
+
if (!pr || pr.headRefName !== CHANGESET_RELEASE_BRANCH)
|
|
3518
|
+
return false;
|
|
3519
|
+
return isChangesetsReleasePr(pr.headRefName, pr.author?.login ?? null, resolveReleaseActor(loadTrustAnchorConfig()));
|
|
3520
|
+
}
|
|
3521
|
+
function main() {
|
|
3522
|
+
if (isExemptChangesetsReleasePr(process.env.PR_NUMBER)) {
|
|
3523
|
+
process.exit(0);
|
|
3524
|
+
}
|
|
3525
|
+
const body = process.env.PR_BODY ?? "";
|
|
3526
|
+
if (!body) {
|
|
3527
|
+
process.exit(0);
|
|
3528
|
+
}
|
|
3529
|
+
const result = checkBareDigits(body);
|
|
3530
|
+
if (result.violations.length === 0) {
|
|
3531
|
+
process.exit(0);
|
|
3532
|
+
}
|
|
3533
|
+
for (const v of result.violations) {
|
|
3534
|
+
emitCheckError({
|
|
3535
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
3536
|
+
check: CHECK_NAME,
|
|
3537
|
+
severity: "error",
|
|
3538
|
+
message: `body-bare-digits: bare digit outside a fenced block, line ${v.line}: ${v.text}`,
|
|
3539
|
+
agent_recovery_prompt: "Move this into a fenced code block, or state it as a symbol reference instead of a narrative claim (e.g. `N`), then re-run `vinaya check body-bare-digits`.",
|
|
3540
|
+
line: v.line
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
process.exit(1);
|
|
3544
|
+
}
|
|
3545
|
+
main();
|