@attalabs/vinaya 0.18.0 → 0.19.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 +2 -0
- package/aeg-root/contracts/architect-planner.md +72 -0
- package/aeg-root/roles/architect.md +91 -0
- package/aeg-root/roles/planner.md +14 -15
- package/aeg-root/tranche-model.md +9 -1
- package/dist/checks/bin/check-body-bare-digits.js +60 -13
- package/dist/checks/bin/check-branch-topology.js +57 -12
- package/dist/checks/bin/check-brief-shape.js +57 -12
- package/dist/checks/bin/check-closes-n.js +57 -12
- package/dist/checks/bin/check-coherence.js +60 -13
- package/dist/checks/bin/check-dead-branch-push.js +57 -12
- package/dist/checks/bin/check-dispatch-readiness.js +127 -40
- package/dist/checks/bin/check-doc-coverage-push.js +60 -13
- package/dist/checks/bin/check-doc-coverage.js +60 -13
- package/dist/checks/bin/check-evidence-fresh.js +57 -12
- package/dist/checks/bin/check-first-push-dispatch.js +128 -41
- package/dist/checks/bin/check-issue-assignment.js +57 -12
- package/dist/checks/bin/check-no-disk-state.js +57 -12
- package/dist/checks/bin/check-reader-resolvable-prose.js +57 -12
- package/dist/checks/bin/check-registry-gates.js +57 -12
- package/dist/checks/bin/check-review-gate.js +60 -13
- package/dist/checks/bin/check-single-plan-pr.js +57 -12
- package/dist/checks/bin/check-test-plan.js +57 -12
- package/dist/index.js +663 -37
- 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/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__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]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0z47hfl._.js → [root-of-the-server]__0ulzsom._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -5
- 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/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2myi4phnshkfw.js → 2r36w820aqwb9.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{415k4k1rf3swg.js → 2xoclb4g7d9n7.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3z3a3gb6he23i.js → 3ocvtlhgckh81.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{8tXGbArNoU7_MriIVYvN0 → RISz5nirLr1mUfMqzSaFS}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{8tXGbArNoU7_MriIVYvN0 → RISz5nirLr1mUfMqzSaFS}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{8tXGbArNoU7_MriIVYvN0 → RISz5nirLr1mUfMqzSaFS}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -15,6 +15,23 @@ function maskCode(body) {
|
|
|
15
15
|
`).map((line) => replaceInlineSpans(line, (span) => " ".repeat(span.length))).join(`
|
|
16
16
|
`);
|
|
17
17
|
}
|
|
18
|
+
var DETAILS_OPEN = /<details\b[^>]*>/gi;
|
|
19
|
+
var DETAILS_CLOSE = /<\/details\s*>/gi;
|
|
20
|
+
function maskDetailsBlocks(body) {
|
|
21
|
+
const fill = (line) => " ".repeat(line.length);
|
|
22
|
+
const lines = body.split(`
|
|
23
|
+
`);
|
|
24
|
+
let depth = 0;
|
|
25
|
+
const out = lines.map((line) => {
|
|
26
|
+
const opens = line.match(DETAILS_OPEN)?.length ?? 0;
|
|
27
|
+
const closes = line.match(DETAILS_CLOSE)?.length ?? 0;
|
|
28
|
+
const maskThisLine = depth > 0 || opens > 0;
|
|
29
|
+
depth = Math.max(0, depth + opens - closes);
|
|
30
|
+
return maskThisLine ? fill(line) : line;
|
|
31
|
+
});
|
|
32
|
+
return out.join(`
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
18
35
|
function stripCode(body, options = {}) {
|
|
19
36
|
const normalised = body.replace(/\r\n?/g, `
|
|
20
37
|
`);
|
|
@@ -281,6 +298,9 @@ function entry(key) {
|
|
|
281
298
|
function label(key) {
|
|
282
299
|
return entry(key).id;
|
|
283
300
|
}
|
|
301
|
+
function trancheLabel(slug) {
|
|
302
|
+
return `${label("tranche")}${slug}`;
|
|
303
|
+
}
|
|
284
304
|
function matchesLabel(key, name) {
|
|
285
305
|
const l = entry(key);
|
|
286
306
|
if (l.form === "prefix")
|
|
@@ -1205,6 +1225,104 @@ function findDeadBranchPushes(facts) {
|
|
|
1205
1225
|
latestCommitAt
|
|
1206
1226
|
}));
|
|
1207
1227
|
}
|
|
1228
|
+
// ../../packages/aeg-core/src/milestone-validation.ts
|
|
1229
|
+
var RELEASE_FIELD = /^\s*(?:\*\*)?Release(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
|
|
1230
|
+
var VALUE_EDGE_MARKUP2 = /^[`.;\s]+|[`.;\s]+$/g;
|
|
1231
|
+
function unwrapValue2(raw) {
|
|
1232
|
+
return raw.replace(/\*\*/g, "").replace(VALUE_EDGE_MARKUP2, "");
|
|
1233
|
+
}
|
|
1234
|
+
var RELEASE_VALUE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
|
|
1235
|
+
function pathText(body) {
|
|
1236
|
+
return stripCode(body, { inlineSpans: "keep" });
|
|
1237
|
+
}
|
|
1238
|
+
function releaseFieldFromText(text) {
|
|
1239
|
+
const m = text.match(RELEASE_FIELD);
|
|
1240
|
+
if (!m)
|
|
1241
|
+
return { declared: false, value: null };
|
|
1242
|
+
const raw = unwrapValue2(m[1] ?? "");
|
|
1243
|
+
return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null };
|
|
1244
|
+
}
|
|
1245
|
+
var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
|
|
1246
|
+
var NEXT_HEADING = /^#{1,6}\s+\S/m;
|
|
1247
|
+
var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
1248
|
+
function intentsBlock(text) {
|
|
1249
|
+
const start = text.match(INTENTS_HEADING);
|
|
1250
|
+
if (!start || start.index === undefined)
|
|
1251
|
+
return null;
|
|
1252
|
+
const rest = text.slice(start.index + start[0].length);
|
|
1253
|
+
const next = rest.match(NEXT_HEADING);
|
|
1254
|
+
const sectionEnd = next && next.index !== undefined ? next.index : rest.length;
|
|
1255
|
+
return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) };
|
|
1256
|
+
}
|
|
1257
|
+
function intentsSectionText(text) {
|
|
1258
|
+
return intentsBlock(text)?.section ?? null;
|
|
1259
|
+
}
|
|
1260
|
+
function parseIntents(text) {
|
|
1261
|
+
const section = intentsSectionText(text);
|
|
1262
|
+
if (section === null)
|
|
1263
|
+
return { intents: [], malformed: false };
|
|
1264
|
+
const intents = [];
|
|
1265
|
+
for (const line of section.split(`
|
|
1266
|
+
`)) {
|
|
1267
|
+
const trimmed = line.trim();
|
|
1268
|
+
if (trimmed.length === 0)
|
|
1269
|
+
continue;
|
|
1270
|
+
const m = trimmed.match(INTENT_BULLET);
|
|
1271
|
+
if (!m)
|
|
1272
|
+
return { intents: [], malformed: true };
|
|
1273
|
+
intents.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
|
|
1274
|
+
}
|
|
1275
|
+
return { intents, malformed: false };
|
|
1276
|
+
}
|
|
1277
|
+
function goalRemainder(text) {
|
|
1278
|
+
const withoutRelease = text.replace(RELEASE_FIELD, "");
|
|
1279
|
+
const block = intentsBlock(withoutRelease);
|
|
1280
|
+
const remainder = block ? withoutRelease.slice(0, block.start) + withoutRelease.slice(block.end) : withoutRelease;
|
|
1281
|
+
return remainder.trim();
|
|
1282
|
+
}
|
|
1283
|
+
function checkMilestoneShape(body) {
|
|
1284
|
+
const errors = [];
|
|
1285
|
+
const text = pathText(body);
|
|
1286
|
+
const release = releaseFieldFromText(text);
|
|
1287
|
+
if (release.declared && release.value === null) {
|
|
1288
|
+
errors.push("milestone-validation Release: the `Release:` field is present but is not a version — " + "write a semver value (e.g. `Release: 1.2.0`), or omit the field entirely for a milestone that declares no version.");
|
|
1289
|
+
}
|
|
1290
|
+
const { intents, malformed } = parseIntents(text);
|
|
1291
|
+
if (malformed) {
|
|
1292
|
+
errors.push("milestone-validation intents: the `### Tranche intents` section does not parse — every non-blank " + "line under it must read `- <slug>: <intent text>`.");
|
|
1293
|
+
}
|
|
1294
|
+
const goal = goalRemainder(text);
|
|
1295
|
+
if (goal.length === 0) {
|
|
1296
|
+
errors.push("milestone-validation goal: no goal text — a milestone description must carry prose beyond its " + "`Release:` field and `### Tranche intents` section.");
|
|
1297
|
+
}
|
|
1298
|
+
if (errors.length > 0)
|
|
1299
|
+
return { status: "fail", errors };
|
|
1300
|
+
return { status: "pass", goal, release: release.value, intents };
|
|
1301
|
+
}
|
|
1302
|
+
function checkAdoptable(facts) {
|
|
1303
|
+
const errors = [];
|
|
1304
|
+
if (!facts.target.exists) {
|
|
1305
|
+
errors.push(`milestone-adopt target: Milestone "${facts.target.title}" does not exist — create it first with \`vinaya milestone create\`.`);
|
|
1306
|
+
} else if (facts.target.state === "closed") {
|
|
1307
|
+
errors.push(`milestone-adopt target: Milestone "${facts.target.title}" is closed — adopt requires an open target.`);
|
|
1308
|
+
}
|
|
1309
|
+
for (const s of facts.slugs) {
|
|
1310
|
+
if (!s.labelExists) {
|
|
1311
|
+
errors.push(`milestone-adopt unknown-slug: no \`vinaya/tranche:${s.slug}\` label exists in this repo.`);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
if (s.issueNumbers.length === 0) {
|
|
1315
|
+
errors.push(`milestone-adopt no-issues: \`vinaya/tranche:${s.slug}\` carries no Issues — nothing to adopt.`);
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
if (s.adoptedElsewhere.length > 0) {
|
|
1319
|
+
errors.push(`milestone-adopt already-adopted: "${s.slug}" is already adopted into "${s.adoptedElsewhere.join('", "')}" — adopt it from there, not from here.`);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (errors.length > 0)
|
|
1323
|
+
return { status: "fail", errors };
|
|
1324
|
+
return { status: "pass" };
|
|
1325
|
+
}
|
|
1208
1326
|
// src/lib/detect.ts
|
|
1209
1327
|
import { execFile as execFile4 } from "node:child_process";
|
|
1210
1328
|
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
@@ -2117,7 +2235,8 @@ var BRIEF_BUILTINS = [
|
|
|
2117
2235
|
"for",
|
|
2118
2236
|
"closesN",
|
|
2119
2237
|
"premiseCoverage",
|
|
2120
|
-
"issueRationale"
|
|
2238
|
+
"issueRationale",
|
|
2239
|
+
"milestoneShape"
|
|
2121
2240
|
];
|
|
2122
2241
|
var BriefSectionSchema = z.union([
|
|
2123
2242
|
z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
|
|
@@ -2128,6 +2247,7 @@ var BriefSectionSchema = z.union([
|
|
|
2128
2247
|
var BriefSchemaSchema = z.object({
|
|
2129
2248
|
pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
2130
2249
|
issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
2250
|
+
milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
2131
2251
|
ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
|
|
2132
2252
|
});
|
|
2133
2253
|
var MANAGED_MANIFEST_VERSION = 2;
|
|
@@ -3041,6 +3161,14 @@ function parseTrancheArgs(args) {
|
|
|
3041
3161
|
const slug = args.find((a) => !a.startsWith("--")) ?? null;
|
|
3042
3162
|
return { slug, yes };
|
|
3043
3163
|
}
|
|
3164
|
+
function trancheArchivalStatus(issues) {
|
|
3165
|
+
if (issues.length === 0)
|
|
3166
|
+
return { kind: "no-tranche" };
|
|
3167
|
+
const openIssues = issues.filter((i) => i.state === "OPEN");
|
|
3168
|
+
if (openIssues.length > 0)
|
|
3169
|
+
return { kind: "open", openIssues };
|
|
3170
|
+
return { kind: "complete" };
|
|
3171
|
+
}
|
|
3044
3172
|
async function runArchiveTranche(args, deps) {
|
|
3045
3173
|
const { slug, yes } = parseTrancheArgs(args);
|
|
3046
3174
|
if (!slug) {
|
|
@@ -3057,23 +3185,39 @@ async function runArchiveTranche(args, deps) {
|
|
|
3057
3185
|
return 1;
|
|
3058
3186
|
}
|
|
3059
3187
|
const repoFlag = `${repo.owner}/${repo.repo}`;
|
|
3060
|
-
const
|
|
3061
|
-
const milestone = milestones.find((m) => m.title === slug);
|
|
3062
|
-
if (!milestone) {
|
|
3063
|
-
console.error(`Error: no open tranche milestone named '${slug}' found in ${repoFlag}.`);
|
|
3064
|
-
return 1;
|
|
3065
|
-
}
|
|
3066
|
-
const openIssues = shJson([
|
|
3188
|
+
const issues = shJson([
|
|
3067
3189
|
"gh",
|
|
3068
|
-
"
|
|
3069
|
-
|
|
3190
|
+
"issue",
|
|
3191
|
+
"list",
|
|
3192
|
+
"-R",
|
|
3193
|
+
repoFlag,
|
|
3194
|
+
"--label",
|
|
3195
|
+
trancheLabel(slug),
|
|
3196
|
+
"--state",
|
|
3197
|
+
"all",
|
|
3198
|
+
"--json",
|
|
3199
|
+
"number,title,state",
|
|
3200
|
+
"--limit",
|
|
3201
|
+
"200"
|
|
3070
3202
|
]);
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
`
|
|
3203
|
+
const status = trancheArchivalStatus(issues);
|
|
3204
|
+
if (status.kind === "no-tranche") {
|
|
3205
|
+
console.error(`Error: no tranche found for '${slug}' in ${repoFlag} — no Issues carry ${trancheLabel(slug)}.`);
|
|
3206
|
+
return 1;
|
|
3207
|
+
}
|
|
3208
|
+
if (status.kind === "open") {
|
|
3209
|
+
console.error(`Error: tranche '${slug}' still has ${status.openIssues.length} open task(s) — refusing to close:
|
|
3210
|
+
` + status.openIssues.map((i) => ` #${i.number} — ${i.title}`).join(`
|
|
3074
3211
|
`));
|
|
3075
3212
|
return 1;
|
|
3076
3213
|
}
|
|
3214
|
+
const milestones = shJson(["gh", "api", `repos/${repoFlag}/milestones?state=open&per_page=100`]);
|
|
3215
|
+
const milestone = milestones.find((m) => m.title === slug);
|
|
3216
|
+
if (!milestone) {
|
|
3217
|
+
process.stdout.write(`Tranche '${slug}' is complete (${issues.length} task(s), all closed) — no open Milestone to close.
|
|
3218
|
+
`);
|
|
3219
|
+
return 0;
|
|
3220
|
+
}
|
|
3077
3221
|
if (!yes) {
|
|
3078
3222
|
const ok = await promptYesNo(`Close tranche '${slug}' (Milestone #${milestone.number}, all tasks closed)?`, false);
|
|
3079
3223
|
closeStdin();
|
|
@@ -5500,7 +5644,11 @@ function runBuiltin(name, input) {
|
|
|
5500
5644
|
for: () => checkForField(body),
|
|
5501
5645
|
closesN: () => checkClosesN(body),
|
|
5502
5646
|
premiseCoverage: () => checkPremiseCoverage(body, changedFiles2),
|
|
5503
|
-
issueRationale: () => checkIssueRationale(body)
|
|
5647
|
+
issueRationale: () => checkIssueRationale(body),
|
|
5648
|
+
milestoneShape: () => {
|
|
5649
|
+
const result = checkMilestoneShape(body);
|
|
5650
|
+
return { errors: result.status === "fail" ? result.errors : [] };
|
|
5651
|
+
}
|
|
5504
5652
|
};
|
|
5505
5653
|
return table[name]().errors;
|
|
5506
5654
|
}
|
|
@@ -5518,7 +5666,8 @@ var BUILTIN_RECOVERY = {
|
|
|
5518
5666
|
for: "Add a `For: <model + environment>` field to the body header block (before the first `##` heading), then re-run `{cmd}`.",
|
|
5519
5667
|
closesN: "Add a `Closes #<N>` reference naming the task Issue to the body, then re-run `{cmd}`.",
|
|
5520
5668
|
premiseCoverage: "Add a `Premise:` assertion whose path matches a file this change touches, then re-run `{cmd}`.",
|
|
5521
|
-
issueRationale: "Add the missing Planner-rationale field named above (every task Issue carries all eight fields), then re-run `{cmd}`."
|
|
5669
|
+
issueRationale: "Add the missing Planner-rationale field named above (every task Issue carries all eight fields), then re-run `{cmd}`.",
|
|
5670
|
+
milestoneShape: "Fix the Milestone description as named above — a goal, an optional well-formed `Release:` field, and an optional parseable `### Tranche intents` section — then re-run `{cmd}`."
|
|
5522
5671
|
};
|
|
5523
5672
|
function escapeRegExp(literal) {
|
|
5524
5673
|
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -5792,6 +5941,238 @@ function issueEditCommand(args) {
|
|
|
5792
5941
|
runGhWrite(["issue", "edit", issueRef], ghArgs, bodyResult, json);
|
|
5793
5942
|
}
|
|
5794
5943
|
|
|
5944
|
+
// src/commands/milestone.ts
|
|
5945
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
5946
|
+
var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
|
|
5947
|
+
var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
|
|
5948
|
+
function sh3(args, input) {
|
|
5949
|
+
return execFileSync11(args[0], args.slice(1), { encoding: "utf8", input, env: process.env }).trim();
|
|
5950
|
+
}
|
|
5951
|
+
function shJson2(args, input) {
|
|
5952
|
+
return JSON.parse(sh3(args, input));
|
|
5953
|
+
}
|
|
5954
|
+
function ghErrorDetail(e) {
|
|
5955
|
+
const stderr = e?.stderr;
|
|
5956
|
+
const text = typeof stderr === "string" ? stderr : stderr?.toString();
|
|
5957
|
+
return (text && text.trim().length > 0 ? text : e?.message ?? "unknown error").trim();
|
|
5958
|
+
}
|
|
5959
|
+
function locateBodyOrRefuse2(args) {
|
|
5960
|
+
let result;
|
|
5961
|
+
try {
|
|
5962
|
+
result = locateBody(args);
|
|
5963
|
+
} catch (e) {
|
|
5964
|
+
if (e instanceof ForgeArgError) {
|
|
5965
|
+
refuse([makeCheckError("forge-args", e.message, `Fix the invocation, then re-run \`${RETRY_CREATE2}\`.`)]);
|
|
5966
|
+
}
|
|
5967
|
+
throw e;
|
|
5968
|
+
}
|
|
5969
|
+
if (result === null) {
|
|
5970
|
+
refuse([
|
|
5971
|
+
makeCheckError("forge-args", "`vinaya milestone create` requires a `--body-file <path>` (or `--body <text>`) carrying the milestone description.", `Add \`--body-file <path>\`, then re-run \`${RETRY_CREATE2}\`.`)
|
|
5972
|
+
]);
|
|
5973
|
+
}
|
|
5974
|
+
return result;
|
|
5975
|
+
}
|
|
5976
|
+
async function milestoneCreateCommand(args) {
|
|
5977
|
+
const json = args.includes("--json");
|
|
5978
|
+
const validateOnly = args.includes("--validate-only");
|
|
5979
|
+
const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
|
|
5980
|
+
const title = extractTitle(rest);
|
|
5981
|
+
if (!title) {
|
|
5982
|
+
refuse([
|
|
5983
|
+
makeCheckError("forge-args", "`vinaya milestone create` requires a `--title <title>` — the Milestone title is free text, never parsed for the version.", `Add \`--title <title>\`, then re-run \`${RETRY_CREATE2}\`.`)
|
|
5984
|
+
]);
|
|
5985
|
+
}
|
|
5986
|
+
const bodyResult = locateBodyOrRefuse2(rest);
|
|
5987
|
+
const body = bodyResult.body;
|
|
5988
|
+
const shape = checkMilestoneShape(body);
|
|
5989
|
+
if (shape.status === "fail") {
|
|
5990
|
+
refuse(shape.errors.map((message) => makeCheckError("milestone-shape", message, `Fix the Milestone description, then re-run \`${RETRY_CREATE2}\`.`)));
|
|
5991
|
+
}
|
|
5992
|
+
const sections = resolveSections("milestone", RETRY_CREATE2);
|
|
5993
|
+
const schemaErrors = validateForgeWrite({ body, title: null, sections, changedFiles: [], retryCommand: RETRY_CREATE2 });
|
|
5994
|
+
if (schemaErrors.length > 0)
|
|
5995
|
+
refuse(schemaErrors);
|
|
5996
|
+
if (validateOnly) {
|
|
5997
|
+
if (json)
|
|
5998
|
+
printJson({ validated: true, written: false, command: "milestone create" });
|
|
5999
|
+
else
|
|
6000
|
+
process.stdout.write(`✓ all brief-schema gates PASS — nothing written (--validate-only).
|
|
6001
|
+
`);
|
|
6002
|
+
return;
|
|
6003
|
+
}
|
|
6004
|
+
const repo = await detectGitRepo();
|
|
6005
|
+
if (!repo?.owner || !repo.repo) {
|
|
6006
|
+
refuse([
|
|
6007
|
+
makeCheckError("forge-fetch", "Could not resolve a GitHub owner/repo from the `origin` remote.", "Run this command from inside a git repository whose `origin` remote points at GitHub.")
|
|
6008
|
+
]);
|
|
6009
|
+
}
|
|
6010
|
+
const repoFlag = `${repo.owner}/${repo.repo}`;
|
|
6011
|
+
let out;
|
|
6012
|
+
try {
|
|
6013
|
+
out = sh3(["gh", "api", `repos/${repoFlag}/milestones`, "--input", "-"], JSON.stringify({ title, description: body }));
|
|
6014
|
+
} catch (e) {
|
|
6015
|
+
refuse([
|
|
6016
|
+
makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CREATE2}\`.`)
|
|
6017
|
+
]);
|
|
6018
|
+
}
|
|
6019
|
+
const created = JSON.parse(out);
|
|
6020
|
+
if (json)
|
|
6021
|
+
printJson({ validated: true, written: true, number: created.number, url: created.html_url });
|
|
6022
|
+
else
|
|
6023
|
+
process.stdout.write(`${created.html_url}
|
|
6024
|
+
`);
|
|
6025
|
+
}
|
|
6026
|
+
function extractTarget(args) {
|
|
6027
|
+
for (let i = 0;i < args.length; i++) {
|
|
6028
|
+
const a = args[i];
|
|
6029
|
+
if (a === "--target" || a === "-m")
|
|
6030
|
+
return args[i + 1] ?? null;
|
|
6031
|
+
if (a.startsWith("--target="))
|
|
6032
|
+
return a.slice("--target=".length);
|
|
6033
|
+
}
|
|
6034
|
+
return null;
|
|
6035
|
+
}
|
|
6036
|
+
function extractSlugs(args) {
|
|
6037
|
+
const slugs = [];
|
|
6038
|
+
for (let i = 0;i < args.length; i++) {
|
|
6039
|
+
const a = args[i];
|
|
6040
|
+
if (a === "--slug") {
|
|
6041
|
+
const v = args[i + 1];
|
|
6042
|
+
if (v)
|
|
6043
|
+
slugs.push(v);
|
|
6044
|
+
} else if (a.startsWith("--slug=")) {
|
|
6045
|
+
slugs.push(a.slice("--slug=".length));
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
return slugs;
|
|
6049
|
+
}
|
|
6050
|
+
function ghJsonOrRefuse(args, what) {
|
|
6051
|
+
try {
|
|
6052
|
+
return shJson2(args);
|
|
6053
|
+
} catch (e) {
|
|
6054
|
+
refuse([
|
|
6055
|
+
makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_ADOPT}\`.`)
|
|
6056
|
+
]);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
function fetchAdoptFacts(repoFlag, target, slugs) {
|
|
6060
|
+
const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels");
|
|
6061
|
+
const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones");
|
|
6062
|
+
const targetMilestone = milestones.find((m) => m.title === target);
|
|
6063
|
+
const targetFacts = {
|
|
6064
|
+
title: target,
|
|
6065
|
+
exists: !!targetMilestone,
|
|
6066
|
+
state: targetMilestone?.state ?? null
|
|
6067
|
+
};
|
|
6068
|
+
const slugFacts = slugs.map((slug) => {
|
|
6069
|
+
const labelExists = labels.some((l) => l.name === trancheLabel(slug));
|
|
6070
|
+
const issues = labelExists ? ghJsonOrRefuse([
|
|
6071
|
+
"gh",
|
|
6072
|
+
"issue",
|
|
6073
|
+
"list",
|
|
6074
|
+
"-R",
|
|
6075
|
+
repoFlag,
|
|
6076
|
+
"--label",
|
|
6077
|
+
trancheLabel(slug),
|
|
6078
|
+
"--state",
|
|
6079
|
+
"all",
|
|
6080
|
+
"--json",
|
|
6081
|
+
"number,state,milestone",
|
|
6082
|
+
"--limit",
|
|
6083
|
+
"200"
|
|
6084
|
+
], `Issues for \`${trancheLabel(slug)}\``) : [];
|
|
6085
|
+
const elsewhere = new Set;
|
|
6086
|
+
for (const issue of issues) {
|
|
6087
|
+
const title = issue.milestone?.title;
|
|
6088
|
+
if (title && title !== slug && title !== target)
|
|
6089
|
+
elsewhere.add(title);
|
|
6090
|
+
}
|
|
6091
|
+
return {
|
|
6092
|
+
slug,
|
|
6093
|
+
labelExists,
|
|
6094
|
+
issueNumbers: issues.map((i) => i.number),
|
|
6095
|
+
adoptedElsewhere: [...elsewhere]
|
|
6096
|
+
};
|
|
6097
|
+
});
|
|
6098
|
+
return { facts: { target: targetFacts, slugs: slugFacts }, milestones };
|
|
6099
|
+
}
|
|
6100
|
+
function failMidWrite(message) {
|
|
6101
|
+
console.error(`
|
|
6102
|
+
[milestone adopt] PARTIAL WRITE — ${message}`);
|
|
6103
|
+
console.error(`[milestone adopt] Re-running \`${RETRY_ADOPT}\` is safe — every write here is idempotent.`);
|
|
6104
|
+
process.exit(1);
|
|
6105
|
+
}
|
|
6106
|
+
async function milestoneAdoptCommand(args) {
|
|
6107
|
+
const json = args.includes("--json");
|
|
6108
|
+
const validateOnly = args.includes("--validate-only");
|
|
6109
|
+
const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
|
|
6110
|
+
const target = extractTarget(rest);
|
|
6111
|
+
if (!target) {
|
|
6112
|
+
refuse([
|
|
6113
|
+
makeCheckError("forge-args", "`vinaya milestone adopt` requires a `--target <title>` — the Milestone every named slug is adopted into.", `Add \`--target <title>\`, then re-run \`${RETRY_ADOPT}\`.`)
|
|
6114
|
+
]);
|
|
6115
|
+
}
|
|
6116
|
+
const slugs = extractSlugs(rest);
|
|
6117
|
+
if (slugs.length === 0) {
|
|
6118
|
+
refuse([
|
|
6119
|
+
makeCheckError("forge-args", "`vinaya milestone adopt` requires at least one `--slug <slug>` — the tranche(s) to move into the target Milestone.", `Add one or more \`--slug <slug>\`, then re-run \`${RETRY_ADOPT}\`.`)
|
|
6120
|
+
]);
|
|
6121
|
+
}
|
|
6122
|
+
const repo = await detectGitRepo();
|
|
6123
|
+
if (!repo?.owner || !repo.repo) {
|
|
6124
|
+
refuse([
|
|
6125
|
+
makeCheckError("forge-fetch", "Could not resolve a GitHub owner/repo from the `origin` remote.", "Run this command from inside a git repository whose `origin` remote points at GitHub.")
|
|
6126
|
+
]);
|
|
6127
|
+
}
|
|
6128
|
+
const repoFlag = `${repo.owner}/${repo.repo}`;
|
|
6129
|
+
const { facts, milestones } = fetchAdoptFacts(repoFlag, target, slugs);
|
|
6130
|
+
const verdict = checkAdoptable(facts);
|
|
6131
|
+
if (verdict.status === "fail") {
|
|
6132
|
+
refuse(verdict.errors.map((message) => makeCheckError("milestone-adopt", message, `Fix the input above, then re-run \`${RETRY_ADOPT}\`.`)));
|
|
6133
|
+
}
|
|
6134
|
+
if (validateOnly) {
|
|
6135
|
+
if (json)
|
|
6136
|
+
printJson({ validated: true, written: false, command: "milestone adopt" });
|
|
6137
|
+
else
|
|
6138
|
+
process.stdout.write(`✓ adopt is safe — nothing written (--validate-only).
|
|
6139
|
+
`);
|
|
6140
|
+
return;
|
|
6141
|
+
}
|
|
6142
|
+
const adopted = [];
|
|
6143
|
+
for (const slugFact of facts.slugs) {
|
|
6144
|
+
for (const issueNumber of slugFact.issueNumbers) {
|
|
6145
|
+
try {
|
|
6146
|
+
sh3(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
|
|
6147
|
+
} catch (e) {
|
|
6148
|
+
failMidWrite(`failed to attach Issue #${issueNumber} (tranche \`${slugFact.slug}\`) to Milestone "${target}": ` + `${ghErrorDetail(e)}. Slugs already adopted this run: ${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
|
|
6152
|
+
let closedMilestone = null;
|
|
6153
|
+
if (legacy) {
|
|
6154
|
+
closedMilestone = legacy.number;
|
|
6155
|
+
if (legacy.state !== "closed") {
|
|
6156
|
+
try {
|
|
6157
|
+
sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
|
|
6158
|
+
} catch (e) {
|
|
6159
|
+
failMidWrite(`attached tranche \`${slugFact.slug}\`'s Issue(s) to Milestone "${target}", but failed to close its old ` + `Milestone #${legacy.number}: ${ghErrorDetail(e)}. Slugs already adopted this run: ` + `${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
|
|
6164
|
+
}
|
|
6165
|
+
if (json)
|
|
6166
|
+
printJson({ validated: true, written: true, target, adopted });
|
|
6167
|
+
else {
|
|
6168
|
+
for (const a of adopted) {
|
|
6169
|
+
const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
|
|
6170
|
+
process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
|
|
6171
|
+
`);
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
|
|
5795
6176
|
// src/commands/new-check.ts
|
|
5796
6177
|
import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5797
6178
|
import { join as join15 } from "node:path";
|
|
@@ -5836,12 +6217,205 @@ ${registration}
|
|
|
5836
6217
|
}
|
|
5837
6218
|
|
|
5838
6219
|
// src/commands/pr.ts
|
|
5839
|
-
import { execFileSync as
|
|
5840
|
-
|
|
6220
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
6221
|
+
|
|
6222
|
+
// src/checks/body-bare-digits-logic.ts
|
|
6223
|
+
function blankRange(text, start, end) {
|
|
6224
|
+
const region = text.slice(start, end);
|
|
6225
|
+
return text.slice(0, start) + region.replace(/[^\n]/g, " ") + text.slice(end);
|
|
6226
|
+
}
|
|
6227
|
+
var EXEMPT_ANCHOR_FIELDS = ["CLOSES", "TIER", "PROJECT", "EVIDENCE"];
|
|
6228
|
+
var FIELD_SECTION_HEADING = {
|
|
6229
|
+
CLOSES: null,
|
|
6230
|
+
PROJECT: null,
|
|
6231
|
+
TIER: /^scope$/i,
|
|
6232
|
+
EVIDENCE: /^evidence$/i
|
|
6233
|
+
};
|
|
6234
|
+
var HEADING = /^#{1,6}\s/;
|
|
6235
|
+
var HEADING_TEXT = /^#{1,6}\s+(.*?)\s*$/;
|
|
6236
|
+
function headerBlockEnd(body) {
|
|
6237
|
+
const lines = body.split(`
|
|
6238
|
+
`);
|
|
6239
|
+
let offset = 0;
|
|
6240
|
+
for (const line of lines) {
|
|
6241
|
+
if (HEADING.test(line))
|
|
6242
|
+
return offset;
|
|
6243
|
+
offset += line.length + 1;
|
|
6244
|
+
}
|
|
6245
|
+
return body.length;
|
|
6246
|
+
}
|
|
6247
|
+
function namedSectionBounds(body, namePattern) {
|
|
6248
|
+
const lines = body.split(`
|
|
6249
|
+
`);
|
|
6250
|
+
let offset = 0;
|
|
6251
|
+
let start = -1;
|
|
6252
|
+
let end = body.length;
|
|
6253
|
+
for (const line of lines) {
|
|
6254
|
+
const headingText = HEADING_TEXT.exec(line);
|
|
6255
|
+
if (start === -1 && headingText && namePattern.test(headingText[1])) {
|
|
6256
|
+
start = offset;
|
|
6257
|
+
} else if (start !== -1 && HEADING.test(line)) {
|
|
6258
|
+
end = offset;
|
|
6259
|
+
break;
|
|
6260
|
+
}
|
|
6261
|
+
offset += line.length + 1;
|
|
6262
|
+
}
|
|
6263
|
+
return start === -1 ? null : { start, end };
|
|
6264
|
+
}
|
|
6265
|
+
function isInCanonicalSection(body, field, outerStart) {
|
|
6266
|
+
const pattern = FIELD_SECTION_HEADING[field];
|
|
6267
|
+
if (pattern === null)
|
|
6268
|
+
return outerStart < headerBlockEnd(body);
|
|
6269
|
+
const bounds = namedSectionBounds(body, pattern);
|
|
6270
|
+
return bounds !== null && outerStart >= bounds.start && outerStart < bounds.end;
|
|
6271
|
+
}
|
|
6272
|
+
var PROJECT_LABEL = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?/i;
|
|
6273
|
+
var FIELD_CONTENT_SIGNATURE = {
|
|
6274
|
+
CLOSES: /Closes\s*#\d+/i,
|
|
6275
|
+
PROJECT: PROJECT_LABEL,
|
|
6276
|
+
TIER: TIER_FIELD,
|
|
6277
|
+
EVIDENCE: /^Head:\s*\S/m
|
|
6278
|
+
};
|
|
6279
|
+
function blankAnchoredRegions(body) {
|
|
6280
|
+
let masked = body;
|
|
6281
|
+
for (const field of EXEMPT_ANCHOR_FIELDS) {
|
|
6282
|
+
const bounds = anchoredRegionBounds(body, field);
|
|
6283
|
+
if (!bounds || !isInCanonicalSection(body, field, bounds.outerStart))
|
|
6284
|
+
continue;
|
|
6285
|
+
const content = body.slice(bounds.innerStart, bounds.innerEnd);
|
|
6286
|
+
if (!FIELD_CONTENT_SIGNATURE[field].test(content))
|
|
6287
|
+
continue;
|
|
6288
|
+
masked = blankRange(masked, bounds.outerStart, bounds.innerStart);
|
|
6289
|
+
masked = blankRange(masked, bounds.innerEnd, bounds.outerEnd);
|
|
6290
|
+
const blankedContent = content.split(`
|
|
6291
|
+
`).map(BOUNDED_ANCHOR_BLANK[field]).join(`
|
|
6292
|
+
`);
|
|
6293
|
+
masked = masked.slice(0, bounds.innerStart) + blankedContent + masked.slice(bounds.innerEnd);
|
|
6294
|
+
}
|
|
6295
|
+
return masked;
|
|
6296
|
+
}
|
|
6297
|
+
function blankTokenReportSection(body) {
|
|
6298
|
+
const lines = body.split(`
|
|
6299
|
+
`);
|
|
6300
|
+
const HEADING2 = /^#{1,6}\s/;
|
|
6301
|
+
const TOKEN_REPORT_HEADING = /^#{1,6}\s*token report\s*$/i;
|
|
6302
|
+
const startIdx = lines.findIndex((l) => TOKEN_REPORT_HEADING.test(l));
|
|
6303
|
+
if (startIdx === -1)
|
|
6304
|
+
return body;
|
|
6305
|
+
let endIdx = lines.length;
|
|
6306
|
+
for (let i = startIdx + 1;i < lines.length; i++) {
|
|
6307
|
+
if (HEADING2.test(lines[i])) {
|
|
6308
|
+
endIdx = i;
|
|
6309
|
+
break;
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
6312
|
+
const fill = (line) => " ".repeat(line.length);
|
|
6313
|
+
for (let i = startIdx;i < endIdx; i++)
|
|
6314
|
+
lines[i] = fill(lines[i]);
|
|
6315
|
+
return lines.join(`
|
|
6316
|
+
`);
|
|
6317
|
+
}
|
|
6318
|
+
function blankTierField(line) {
|
|
6319
|
+
const m = TIER_FIELD.exec(line);
|
|
6320
|
+
if (!m)
|
|
6321
|
+
return line;
|
|
6322
|
+
return line.slice(0, m.index) + " ".repeat(m[0].length) + line.slice(m.index + m[0].length);
|
|
6323
|
+
}
|
|
6324
|
+
var SEGMENT_HAS_DIGIT = /\p{Nd}/u;
|
|
6325
|
+
function blankProjectField(line) {
|
|
6326
|
+
const label2 = PROJECT_LABEL.exec(line);
|
|
6327
|
+
if (!label2)
|
|
6328
|
+
return line;
|
|
6329
|
+
const valueStart = label2.index + label2[0].length;
|
|
6330
|
+
const segments = line.slice(valueStart).split(",");
|
|
6331
|
+
const blanked = segments.map((seg) => {
|
|
6332
|
+
const name = unwrapValue(seg);
|
|
6333
|
+
const isRealName = PROJECT_SLUG.test(name) && !SEGMENT_HAS_DIGIT.test(name);
|
|
6334
|
+
return isRealName ? " ".repeat(seg.length) : seg;
|
|
6335
|
+
});
|
|
6336
|
+
return line.slice(0, valueStart) + blanked.join(",");
|
|
6337
|
+
}
|
|
6338
|
+
function blankUnanchoredStructuralFields(body) {
|
|
6339
|
+
return body.split(`
|
|
6340
|
+
`).map((l) => blankProjectField(blankTierField(l))).join(`
|
|
6341
|
+
`);
|
|
6342
|
+
}
|
|
6343
|
+
var CLOSES_REF = /Closes\s*#\d+/i;
|
|
6344
|
+
var EVIDENCE_HEADING = /^#{1,6}\s/;
|
|
6345
|
+
var EVIDENCE_HEAD_LINE = /^Head:\s*\S+$/i;
|
|
6346
|
+
function blankClosesField(line) {
|
|
6347
|
+
const m = CLOSES_REF.exec(line);
|
|
6348
|
+
if (!m)
|
|
6349
|
+
return line;
|
|
6350
|
+
return line.slice(0, m.index) + " ".repeat(m[0].length) + line.slice(m.index + m[0].length);
|
|
6351
|
+
}
|
|
6352
|
+
function blankEvidenceField(line) {
|
|
6353
|
+
const trimmed = line.trim();
|
|
6354
|
+
if (trimmed === "" || EVIDENCE_HEAD_LINE.test(trimmed) || EVIDENCE_HEADING.test(trimmed))
|
|
6355
|
+
return " ".repeat(line.length);
|
|
6356
|
+
return line;
|
|
6357
|
+
}
|
|
6358
|
+
var BOUNDED_ANCHOR_BLANK = {
|
|
6359
|
+
CLOSES: blankClosesField,
|
|
6360
|
+
TIER: blankTierField,
|
|
6361
|
+
PROJECT: blankProjectField,
|
|
6362
|
+
EVIDENCE: blankEvidenceField
|
|
6363
|
+
};
|
|
6364
|
+
function buildScanMask(body) {
|
|
6365
|
+
let masked = maskDetailsBlocks(maskCode(body));
|
|
6366
|
+
masked = blankAnchoredRegions(masked);
|
|
6367
|
+
masked = blankTokenReportSection(masked);
|
|
6368
|
+
masked = blankUnanchoredStructuralFields(masked);
|
|
6369
|
+
return masked;
|
|
6370
|
+
}
|
|
6371
|
+
var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
|
|
6372
|
+
var LIST_MARKER_TOKEN = /^\p{Nd}{1,9}[.)]$/u;
|
|
6373
|
+
var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
6374
|
+
var HTML_ENTITIES = {
|
|
6375
|
+
""": '"',
|
|
6376
|
+
"'": "'",
|
|
6377
|
+
"<": "<",
|
|
6378
|
+
">": ">",
|
|
6379
|
+
"&": "&"
|
|
6380
|
+
};
|
|
6381
|
+
function decodeNamedEntities(body) {
|
|
6382
|
+
return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
|
|
6383
|
+
}
|
|
6384
|
+
function isLineLeadingListMarker(line, matchStart, rawToken) {
|
|
6385
|
+
if (!LIST_MARKER_TOKEN.test(rawToken))
|
|
6386
|
+
return false;
|
|
6387
|
+
const before = line.slice(0, matchStart);
|
|
6388
|
+
return /^ {0,3}$/.test(before);
|
|
6389
|
+
}
|
|
6390
|
+
function checkBareDigits(rawBody) {
|
|
6391
|
+
const body = decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
|
|
6392
|
+
const masked = buildScanMask(body);
|
|
6393
|
+
const maskedLines = masked.split(`
|
|
6394
|
+
`);
|
|
6395
|
+
const origLines = body.split(`
|
|
6396
|
+
`);
|
|
6397
|
+
const violations = [];
|
|
6398
|
+
for (let i = 0;i < maskedLines.length; i++) {
|
|
6399
|
+
const maskedLine = maskedLines[i];
|
|
6400
|
+
const origLine = origLines[i];
|
|
6401
|
+
TOKEN_WITH_DIGIT.lastIndex = 0;
|
|
6402
|
+
let m;
|
|
6403
|
+
while ((m = TOKEN_WITH_DIGIT.exec(maskedLine)) !== null) {
|
|
6404
|
+
const rawToken = origLine.slice(m.index, m.index + m[0].length);
|
|
6405
|
+
if (isLineLeadingListMarker(maskedLine, m.index, rawToken))
|
|
6406
|
+
continue;
|
|
6407
|
+
violations.push({ line: i + 1, text: origLine.trim() });
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
return { violations };
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6413
|
+
// src/commands/pr.ts
|
|
6414
|
+
var RETRY_CREATE3 = "vinaya pr create --validate-only …";
|
|
5841
6415
|
var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
|
|
5842
6416
|
function git3(args) {
|
|
5843
6417
|
try {
|
|
5844
|
-
return
|
|
6418
|
+
return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5845
6419
|
} catch {
|
|
5846
6420
|
return "";
|
|
5847
6421
|
}
|
|
@@ -5854,7 +6428,7 @@ function localChangedFiles() {
|
|
|
5854
6428
|
return out.split(`
|
|
5855
6429
|
`).map((s) => s.trim()).filter(Boolean);
|
|
5856
6430
|
}
|
|
5857
|
-
function
|
|
6431
|
+
function locateBodyOrRefuse3(ghArgs, retryCommand) {
|
|
5858
6432
|
try {
|
|
5859
6433
|
return locateBody(ghArgs);
|
|
5860
6434
|
} catch (e) {
|
|
@@ -5875,7 +6449,7 @@ function reportPass2(json, command) {
|
|
|
5875
6449
|
function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
5876
6450
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
5877
6451
|
try {
|
|
5878
|
-
const out =
|
|
6452
|
+
const out = execFileSync12("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
5879
6453
|
const url = out.trim();
|
|
5880
6454
|
if (json)
|
|
5881
6455
|
printJson({ validated: true, written: true, url });
|
|
@@ -5889,7 +6463,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
|
5889
6463
|
function fetchPrForgeContext(prRef) {
|
|
5890
6464
|
let viewOut;
|
|
5891
6465
|
try {
|
|
5892
|
-
viewOut =
|
|
6466
|
+
viewOut = execFileSync12("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
|
|
5893
6467
|
encoding: "utf8",
|
|
5894
6468
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5895
6469
|
});
|
|
@@ -5914,19 +6488,25 @@ function fetchPrForgeContext(prRef) {
|
|
|
5914
6488
|
const changedFiles2 = (parsed.files ?? []).map((f) => f.path);
|
|
5915
6489
|
return { changedFiles: changedFiles2, branch: parsed.headRefName ?? "" };
|
|
5916
6490
|
}
|
|
6491
|
+
function refuseOnBareDigits(body, retryCommand) {
|
|
6492
|
+
const { violations } = checkBareDigits(body);
|
|
6493
|
+
if (violations.length === 0)
|
|
6494
|
+
return;
|
|
6495
|
+
refuse(violations.map((v) => makeCheckError("body-bare-digits", `body-bare-digits: bare digit outside a fenced block, line ${v.line}: ${v.text}`, `Backtick the digit, move it into a fenced block, or state it as a symbol, then re-run \`${retryCommand}\`.`)));
|
|
6496
|
+
}
|
|
5917
6497
|
function prCreateCommand(args) {
|
|
5918
6498
|
const json = args.includes("--json");
|
|
5919
6499
|
const validateOnly = args.includes("--validate-only");
|
|
5920
6500
|
const ghArgs = args.filter((a) => a !== "--json" && a !== "--validate-only");
|
|
5921
|
-
const bodyResult =
|
|
6501
|
+
const bodyResult = locateBodyOrRefuse3(ghArgs, RETRY_CREATE3);
|
|
5922
6502
|
const body = bodyResult?.body ?? null;
|
|
5923
6503
|
const title = extractTitle(ghArgs);
|
|
5924
6504
|
if (body === null) {
|
|
5925
6505
|
refuse([
|
|
5926
|
-
makeCheckError("forge-args", "No `--body-file <path>` (or `--body`) argument found — the brief-schema gate needs the PR body to validate it.", `Add \`--body-file <path>\` (or \`--body\`), then re-run \`${
|
|
6506
|
+
makeCheckError("forge-args", "No `--body-file <path>` (or `--body`) argument found — the brief-schema gate needs the PR body to validate it.", `Add \`--body-file <path>\` (or \`--body\`), then re-run \`${RETRY_CREATE3}\`.`)
|
|
5927
6507
|
]);
|
|
5928
6508
|
}
|
|
5929
|
-
const sections = resolveSections("pr",
|
|
6509
|
+
const sections = resolveSections("pr", RETRY_CREATE3);
|
|
5930
6510
|
const changedFiles2 = localChangedFiles();
|
|
5931
6511
|
const headCommit = git3(["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
5932
6512
|
const branch = headCommit === "" ? "" : git3(["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
@@ -5935,11 +6515,12 @@ function prCreateCommand(args) {
|
|
|
5935
6515
|
title,
|
|
5936
6516
|
sections,
|
|
5937
6517
|
changedFiles: changedFiles2,
|
|
5938
|
-
retryCommand:
|
|
6518
|
+
retryCommand: RETRY_CREATE3,
|
|
5939
6519
|
branch
|
|
5940
6520
|
});
|
|
5941
6521
|
if (errors.length > 0)
|
|
5942
6522
|
refuse(errors);
|
|
6523
|
+
refuseOnBareDigits(body, RETRY_CREATE3);
|
|
5943
6524
|
if (validateOnly) {
|
|
5944
6525
|
reportPass2(json, "pr create");
|
|
5945
6526
|
return;
|
|
@@ -5957,7 +6538,7 @@ function prEditCommand(args) {
|
|
|
5957
6538
|
]);
|
|
5958
6539
|
}
|
|
5959
6540
|
const ghArgs = rest.slice(1);
|
|
5960
|
-
const bodyResult =
|
|
6541
|
+
const bodyResult = locateBodyOrRefuse3(ghArgs, RETRY_EDIT2);
|
|
5961
6542
|
const body = bodyResult?.body ?? null;
|
|
5962
6543
|
const title = extractTitle(ghArgs);
|
|
5963
6544
|
if (body === null && title === null) {
|
|
@@ -5983,6 +6564,8 @@ function prEditCommand(args) {
|
|
|
5983
6564
|
});
|
|
5984
6565
|
if (errors.length > 0)
|
|
5985
6566
|
refuse(errors);
|
|
6567
|
+
if (body !== null)
|
|
6568
|
+
refuseOnBareDigits(body, RETRY_EDIT2);
|
|
5986
6569
|
if (validateOnly) {
|
|
5987
6570
|
reportPass2(json, "pr edit");
|
|
5988
6571
|
return;
|
|
@@ -5991,14 +6574,14 @@ function prEditCommand(args) {
|
|
|
5991
6574
|
}
|
|
5992
6575
|
|
|
5993
6576
|
// src/commands/pr-report.ts
|
|
5994
|
-
import { execFileSync as
|
|
6577
|
+
import { execFileSync as execFileSync13, spawnSync as spawnSync2 } from "node:child_process";
|
|
5995
6578
|
import { existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5996
6579
|
import { join as join16 } from "node:path";
|
|
5997
6580
|
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
5998
6581
|
var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
|
|
5999
6582
|
function git4(args) {
|
|
6000
6583
|
try {
|
|
6001
|
-
return
|
|
6584
|
+
return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
6002
6585
|
} catch {
|
|
6003
6586
|
return "";
|
|
6004
6587
|
}
|
|
@@ -6016,7 +6599,7 @@ class GitCommandError extends Error {
|
|
|
6016
6599
|
}
|
|
6017
6600
|
function gitStrict(args) {
|
|
6018
6601
|
try {
|
|
6019
|
-
return
|
|
6602
|
+
return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6020
6603
|
} catch (err) {
|
|
6021
6604
|
const stderr = err.stderr;
|
|
6022
6605
|
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
@@ -6170,7 +6753,7 @@ async function prReportCommand(args) {
|
|
|
6170
6753
|
}
|
|
6171
6754
|
|
|
6172
6755
|
// src/commands/quickstart.ts
|
|
6173
|
-
import { execFileSync as
|
|
6756
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
6174
6757
|
import { existsSync as existsSync17, readFileSync as readFileSync17 } from "node:fs";
|
|
6175
6758
|
import { join as join18 } from "node:path";
|
|
6176
6759
|
|
|
@@ -6366,7 +6949,7 @@ function realDeps6() {
|
|
|
6366
6949
|
};
|
|
6367
6950
|
}
|
|
6368
6951
|
function execGit(repoRoot2, args) {
|
|
6369
|
-
return
|
|
6952
|
+
return execFileSync14("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6370
6953
|
}
|
|
6371
6954
|
function errorDetail(err) {
|
|
6372
6955
|
const stderr = err.stderr;
|
|
@@ -6483,7 +7066,7 @@ async function quickstartCommand(args) {
|
|
|
6483
7066
|
}
|
|
6484
7067
|
|
|
6485
7068
|
// src/commands/review-post.ts
|
|
6486
|
-
import { execFileSync as
|
|
7069
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
6487
7070
|
import { readFileSync as readFileSync18, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
|
|
6488
7071
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
6489
7072
|
import { join as join19 } from "node:path";
|
|
@@ -6690,7 +7273,7 @@ function normalizeSecurityVerdict(raw) {
|
|
|
6690
7273
|
}
|
|
6691
7274
|
function gh(args) {
|
|
6692
7275
|
try {
|
|
6693
|
-
return
|
|
7276
|
+
return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6694
7277
|
} catch (err) {
|
|
6695
7278
|
const stderr = err.stderr;
|
|
6696
7279
|
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
@@ -7402,12 +7985,12 @@ async function upgradeCommand(args) {
|
|
|
7402
7985
|
}
|
|
7403
7986
|
|
|
7404
7987
|
// src/commands/waiver.ts
|
|
7405
|
-
import { execFileSync as
|
|
7988
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
7406
7989
|
function labelFor(kind) {
|
|
7407
7990
|
return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
|
|
7408
7991
|
}
|
|
7409
7992
|
function gh2(args) {
|
|
7410
|
-
return
|
|
7993
|
+
return execFileSync16("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
|
|
7411
7994
|
}
|
|
7412
7995
|
function detectPrNumber() {
|
|
7413
7996
|
try {
|
|
@@ -7507,8 +8090,8 @@ Reason: ${reason}`];
|
|
|
7507
8090
|
}
|
|
7508
8091
|
process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
|
|
7509
8092
|
`);
|
|
7510
|
-
|
|
7511
|
-
|
|
8093
|
+
execFileSync16("gh", editArgs, { stdio: "inherit", env: process.env });
|
|
8094
|
+
execFileSync16("gh", commentArgs, { stdio: "inherit", env: process.env });
|
|
7512
8095
|
process.stdout.write(`
|
|
7513
8096
|
✓ Label applied and reason posted as a PR comment.
|
|
7514
8097
|
`);
|
|
@@ -7668,6 +8251,37 @@ var COMMANDS = [
|
|
|
7668
8251
|
],
|
|
7669
8252
|
status: "shipped"
|
|
7670
8253
|
},
|
|
8254
|
+
{
|
|
8255
|
+
name: "milestone create",
|
|
8256
|
+
description: "Create a GitHub Milestone from a validated body",
|
|
8257
|
+
flags: [
|
|
8258
|
+
{ flag: "--title", description: "Milestone title — free text, never parsed for a version" },
|
|
8259
|
+
{
|
|
8260
|
+
flag: "--body-file",
|
|
8261
|
+
description: "Path to the Milestone description (stream-safe; same bytes validated and sent)"
|
|
8262
|
+
},
|
|
8263
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without creating the Milestone" },
|
|
8264
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
8265
|
+
],
|
|
8266
|
+
details: [
|
|
8267
|
+
"Refuses before any `gh` call when the goal is absent, an optional `Release:` field is present but not a version, or an optional `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `Release:` is the sole authority for the milestone's version — the title is never parsed for one."
|
|
8268
|
+
],
|
|
8269
|
+
status: "shipped"
|
|
8270
|
+
},
|
|
8271
|
+
{
|
|
8272
|
+
name: "milestone adopt",
|
|
8273
|
+
description: "Move one or more existing tranches into a target Milestone",
|
|
8274
|
+
flags: [
|
|
8275
|
+
{ flag: "--target", description: "The Milestone every named slug is adopted into" },
|
|
8276
|
+
{ flag: "--slug", description: "A tranche slug to adopt — repeatable for a multi-slug move" },
|
|
8277
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without writing anything" },
|
|
8278
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
8279
|
+
],
|
|
8280
|
+
details: [
|
|
8281
|
+
"Reattaches every Issue carrying each named `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact for every named slug is gathered and checked in ONE call before any write, so a single unsafe slug — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone — refuses the whole invocation, not just its own slug."
|
|
8282
|
+
],
|
|
8283
|
+
status: "shipped"
|
|
8284
|
+
},
|
|
7671
8285
|
{
|
|
7672
8286
|
name: "review post",
|
|
7673
8287
|
description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
|
|
@@ -7966,6 +8580,18 @@ try {
|
|
|
7966
8580
|
}
|
|
7967
8581
|
break;
|
|
7968
8582
|
}
|
|
8583
|
+
case "milestone": {
|
|
8584
|
+
const [subcommand, ...rest] = args;
|
|
8585
|
+
if (subcommand === "create") {
|
|
8586
|
+
await milestoneCreateCommand(rest);
|
|
8587
|
+
} else if (subcommand === "adopt") {
|
|
8588
|
+
await milestoneAdoptCommand(rest);
|
|
8589
|
+
} else {
|
|
8590
|
+
console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected 'create' or 'adopt')`);
|
|
8591
|
+
process.exit(2);
|
|
8592
|
+
}
|
|
8593
|
+
break;
|
|
8594
|
+
}
|
|
7969
8595
|
case "review": {
|
|
7970
8596
|
const [subcommand, ...rest] = args;
|
|
7971
8597
|
if (subcommand === "post") {
|