@attalabs/vinaya 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aeg-root/enforcement.md +2 -2
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/skills/brief-authoring/SKILL.md +1 -1
- package/aeg-root/state-machine.md +6 -6
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-branch-topology.js +21 -5
- package/dist/checks/bin/check-brief-shape.js +21 -5
- package/dist/checks/bin/check-closes-n.js +21 -5
- package/dist/checks/bin/check-coherence.js +200 -10
- package/dist/checks/bin/check-dead-branch-push.js +21 -5
- package/dist/checks/bin/check-dispatch-readiness.js +223 -11
- package/dist/checks/bin/check-doc-coverage-push.js +21 -5
- package/dist/checks/bin/check-doc-coverage.js +55 -10
- package/dist/checks/bin/check-first-push-dispatch.js +224 -12
- package/dist/checks/bin/check-issue-assignment.js +21 -5
- package/dist/checks/bin/check-no-disk-state.js +21 -5
- package/dist/checks/bin/check-reader-resolvable-prose.js +21 -5
- package/dist/checks/bin/check-registry-gates.js +21 -5
- package/dist/checks/bin/check-review-gate.js +21 -5
- package/dist/checks/bin/check-single-plan-pr.js +21 -5
- package/dist/checks/bin/check-test-plan.js +21 -5
- package/dist/index.js +6 -2
- package/package.json +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/checks/bin/check-dispatch-readiness.ts
|
|
4
|
-
import { execFile as execFile4, execFileSync as
|
|
4
|
+
import { execFile as execFile4, execFileSync as execFileSync3 } from "node:child_process";
|
|
5
5
|
import { promisify as promisify4 } from "node:util";
|
|
6
6
|
// ../../packages/aeg-core/src/anchored-region.ts
|
|
7
7
|
function maskCode(body) {
|
|
@@ -1219,7 +1219,8 @@ function mapForgeFacts(raw) {
|
|
|
1219
1219
|
reviewDecision: mapReviewDecision(raw.pullRequest?.reviewDecision),
|
|
1220
1220
|
stateReason: mapStateReason(raw.issue.stateReason),
|
|
1221
1221
|
closedAt: raw.issue.closedAt ?? null,
|
|
1222
|
-
mergedAt: raw.pullRequest?.mergedAt ?? null
|
|
1222
|
+
mergedAt: raw.pullRequest?.mergedAt ?? null,
|
|
1223
|
+
closedByActor: raw.closedByActor ?? null
|
|
1223
1224
|
};
|
|
1224
1225
|
}
|
|
1225
1226
|
function mapStateReason(reason) {
|
|
@@ -1312,6 +1313,7 @@ function buildBatchQuery(tranche, tasks) {
|
|
|
1312
1313
|
timelineItems(last: 1, itemTypes: [CLOSED_EVENT]) {
|
|
1313
1314
|
nodes {
|
|
1314
1315
|
... on ClosedEvent {
|
|
1316
|
+
actor { login }
|
|
1315
1317
|
closer {
|
|
1316
1318
|
... on PullRequest {
|
|
1317
1319
|
number
|
|
@@ -1349,7 +1351,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1349
1351
|
const issue = repository[`${alias}_issue`];
|
|
1350
1352
|
const ref = repository[`${alias}_ref`];
|
|
1351
1353
|
const prs = repository[`${alias}_prs`];
|
|
1352
|
-
const
|
|
1354
|
+
const timelineNode = issue?.timelineItems?.nodes?.[0];
|
|
1355
|
+
const rawCloser = timelineNode?.closer ?? null;
|
|
1353
1356
|
const closingPr = rawCloser && typeof rawCloser.number === "number" && typeof rawCloser.url === "string" ? rawCloser : null;
|
|
1354
1357
|
const branchPr = prs && prs.nodes.length > 0 && prs.nodes[0] ? prs.nodes[0] : null;
|
|
1355
1358
|
return {
|
|
@@ -1361,7 +1364,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1361
1364
|
labels: issue.labels.nodes.map((n) => n.name)
|
|
1362
1365
|
} : null,
|
|
1363
1366
|
refExists: Boolean(ref && ref.name.length > 0),
|
|
1364
|
-
pullRequest: closingPr ?? branchPr
|
|
1367
|
+
pullRequest: closingPr ?? branchPr,
|
|
1368
|
+
closedByActor: timelineNode?.actor?.login ?? null
|
|
1365
1369
|
};
|
|
1366
1370
|
}
|
|
1367
1371
|
function describeError(err) {
|
|
@@ -1499,6 +1503,11 @@ var FORGE_FACT_INPUTS = [
|
|
|
1499
1503
|
fact: "mergedAt",
|
|
1500
1504
|
readsFrom: "PullRequest.mergedAt",
|
|
1501
1505
|
meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
fact: "closedByActor",
|
|
1509
|
+
readsFrom: "ClosedEvent.actor.login (most recent timeline CLOSED_EVENT)",
|
|
1510
|
+
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."
|
|
1502
1511
|
}
|
|
1503
1512
|
];
|
|
1504
1513
|
var DERIVED_STATUSES = [
|
|
@@ -1978,7 +1987,7 @@ function isGrandfathered(isoDate) {
|
|
|
1978
1987
|
return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM;
|
|
1979
1988
|
}
|
|
1980
1989
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
1981
|
-
function checkA1(entries) {
|
|
1990
|
+
function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
|
|
1982
1991
|
const failures = [];
|
|
1983
1992
|
for (const e of entries) {
|
|
1984
1993
|
if (!e.facts)
|
|
@@ -1986,6 +1995,9 @@ function checkA1(entries) {
|
|
|
1986
1995
|
if (e.facts.stateReason === "not_planned")
|
|
1987
1996
|
continue;
|
|
1988
1997
|
if (e.facts.issueState === "closed" && e.facts.prState !== "merged") {
|
|
1998
|
+
const handClosed = e.facts.stateReason === "completed" && isPrincipal(e.facts.closedByActor, principalAllowlist);
|
|
1999
|
+
if (handClosed)
|
|
2000
|
+
continue;
|
|
1989
2001
|
failures.push({
|
|
1990
2002
|
issue: e.task.issue,
|
|
1991
2003
|
tranche: e.trancheSlug,
|
|
@@ -2675,9 +2687,13 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
2675
2687
|
return terms;
|
|
2676
2688
|
}
|
|
2677
2689
|
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2690
|
+
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2691
|
+
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
2692
|
+
}
|
|
2678
2693
|
function checkDispatchReadiness(input) {
|
|
2679
2694
|
const { trancheSlug, task } = input;
|
|
2680
2695
|
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
2696
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2681
2697
|
const blockers = [];
|
|
2682
2698
|
if (task.issue === null) {
|
|
2683
2699
|
blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
|
|
@@ -2688,7 +2704,7 @@ function checkDispatchReadiness(input) {
|
|
|
2688
2704
|
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.`);
|
|
2689
2705
|
}
|
|
2690
2706
|
for (const dep of input.dependsOn) {
|
|
2691
|
-
if (!dep.merged) {
|
|
2707
|
+
if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
|
|
2692
2708
|
const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
|
|
2693
2709
|
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
|
|
2694
2710
|
}
|
|
@@ -2944,12 +2960,186 @@ function emitCheckError(error) {
|
|
|
2944
2960
|
`);
|
|
2945
2961
|
}
|
|
2946
2962
|
|
|
2963
|
+
// src/lib/config.ts
|
|
2964
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2965
|
+
import { homedir } from "node:os";
|
|
2966
|
+
import { dirname, join } from "node:path";
|
|
2967
|
+
import { z as z2 } from "zod";
|
|
2968
|
+
var EnvEntrySchema = z2.union([
|
|
2969
|
+
z2.literal(true),
|
|
2970
|
+
z2.object({ optional: z2.literal(true) }),
|
|
2971
|
+
z2.object({ anyOf: z2.array(z2.string()).min(2) }),
|
|
2972
|
+
z2.string()
|
|
2973
|
+
]);
|
|
2974
|
+
var CheckEntrySchema = z2.object({
|
|
2975
|
+
run: z2.string(),
|
|
2976
|
+
scope: z2.enum(["diff", "full"]),
|
|
2977
|
+
include: z2.array(z2.string()).optional(),
|
|
2978
|
+
args: z2.array(z2.string()).optional(),
|
|
2979
|
+
timeoutMs: z2.number().optional(),
|
|
2980
|
+
env: z2.record(z2.string(), EnvEntrySchema).optional(),
|
|
2981
|
+
requiresOpenPr: z2.boolean().optional(),
|
|
2982
|
+
ownWorkflow: z2.boolean().optional()
|
|
2983
|
+
}).superRefine((entry2, ctx) => {
|
|
2984
|
+
if (!entry2.env)
|
|
2985
|
+
return;
|
|
2986
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
2987
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
2988
|
+
continue;
|
|
2989
|
+
const members = value.anyOf;
|
|
2990
|
+
if (new Set(members).size !== members.length) {
|
|
2991
|
+
ctx.addIssue({
|
|
2992
|
+
code: z2.ZodIssueCode.custom,
|
|
2993
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
2994
|
+
path: ["env", key, "anyOf"]
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
if (!members.includes(key)) {
|
|
2998
|
+
ctx.addIssue({
|
|
2999
|
+
code: z2.ZodIssueCode.custom,
|
|
3000
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
3001
|
+
path: ["env", key, "anyOf"]
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
3006
|
+
var BRIEF_BUILTINS = [
|
|
3007
|
+
"tier",
|
|
3008
|
+
"testPlan",
|
|
3009
|
+
"testPlanExclusivity",
|
|
3010
|
+
"principalPlaceholder",
|
|
3011
|
+
"surfaceMap",
|
|
3012
|
+
"docUpdateList",
|
|
3013
|
+
"worktreeStep0",
|
|
3014
|
+
"stopConditions",
|
|
3015
|
+
"autonomyClause",
|
|
3016
|
+
"project",
|
|
3017
|
+
"for",
|
|
3018
|
+
"closesN",
|
|
3019
|
+
"premiseCoverage",
|
|
3020
|
+
"issueRationale"
|
|
3021
|
+
];
|
|
3022
|
+
var BriefSectionSchema = z2.union([
|
|
3023
|
+
z2.object({ builtin: z2.enum(BRIEF_BUILTINS) }),
|
|
3024
|
+
z2.object({ heading: z2.string().min(1), name: z2.string().optional() }),
|
|
3025
|
+
z2.object({ field: z2.string().min(1), name: z2.string().optional() }),
|
|
3026
|
+
z2.object({ phrase: z2.string().min(1), name: z2.string().optional() })
|
|
3027
|
+
]);
|
|
3028
|
+
var BriefSchemaSchema = z2.object({
|
|
3029
|
+
pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
|
|
3030
|
+
issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional()
|
|
3031
|
+
});
|
|
3032
|
+
function isSafeRepoRelPath(p) {
|
|
3033
|
+
if (p.length === 0)
|
|
3034
|
+
return false;
|
|
3035
|
+
if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
|
|
3036
|
+
return false;
|
|
3037
|
+
return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
|
|
3038
|
+
}
|
|
3039
|
+
var SafeRepoRelPath = z2.string().refine(isSafeRepoRelPath, {
|
|
3040
|
+
message: "must be a repo-root-relative path with no `..` segment or absolute root"
|
|
3041
|
+
});
|
|
3042
|
+
var ManagedBlockRecordSchema = z2.object({
|
|
3043
|
+
path: SafeRepoRelPath,
|
|
3044
|
+
marker: z2.string(),
|
|
3045
|
+
comment: z2.enum(["hash", "html"])
|
|
3046
|
+
});
|
|
3047
|
+
var ManagedManifestSchema = z2.object({
|
|
3048
|
+
version: z2.number().int().positive(),
|
|
3049
|
+
files: z2.array(SafeRepoRelPath),
|
|
3050
|
+
blocks: z2.array(ManagedBlockRecordSchema),
|
|
3051
|
+
labels: z2.array(z2.string())
|
|
3052
|
+
});
|
|
3053
|
+
var VinayaConfigSchema = z2.object({
|
|
3054
|
+
rings: z2.object({
|
|
3055
|
+
ring1_forgeWriteInterception: z2.boolean(),
|
|
3056
|
+
ring2_asyncAudits: z2.boolean()
|
|
3057
|
+
}).optional(),
|
|
3058
|
+
checks: z2.record(z2.string(), CheckEntrySchema).optional(),
|
|
3059
|
+
briefSchema: BriefSchemaSchema.optional(),
|
|
3060
|
+
managed: ManagedManifestSchema.optional(),
|
|
3061
|
+
principals: z2.array(z2.string()).min(1).optional(),
|
|
3062
|
+
ci: z2.object({ setup: z2.string().min(1) }).optional()
|
|
3063
|
+
});
|
|
3064
|
+
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
3065
|
+
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
3066
|
+
var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
|
|
3067
|
+
function resolvePrincipalAllowlist(config) {
|
|
3068
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
3069
|
+
}
|
|
3070
|
+
function trustAnchorRepo() {
|
|
3071
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
3072
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
3073
|
+
if (fromRunner) {
|
|
3074
|
+
const validated = wellFormed(fromRunner);
|
|
3075
|
+
if (validated)
|
|
3076
|
+
return validated;
|
|
3077
|
+
}
|
|
3078
|
+
try {
|
|
3079
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
3080
|
+
encoding: "utf-8",
|
|
3081
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3082
|
+
}).trim();
|
|
3083
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
3084
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
3085
|
+
} catch {
|
|
3086
|
+
return null;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
function ghFetchTrustAnchorConfig() {
|
|
3090
|
+
const repo = trustAnchorRepo();
|
|
3091
|
+
if (!repo)
|
|
3092
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
3093
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
3094
|
+
encoding: "utf-8",
|
|
3095
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
function firstLine(err) {
|
|
3099
|
+
return err?.message?.split(`
|
|
3100
|
+
`)[0] ?? "unknown error";
|
|
3101
|
+
}
|
|
3102
|
+
function isMissingFileError(err) {
|
|
3103
|
+
const stderr = err?.stderr;
|
|
3104
|
+
const haystack = [
|
|
3105
|
+
err?.message ?? "",
|
|
3106
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
3107
|
+
].join(`
|
|
3108
|
+
`);
|
|
3109
|
+
return /\b404\b|not found/i.test(haystack);
|
|
3110
|
+
}
|
|
3111
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
3112
|
+
const warn = (why) => process.stdout.write(`⚠ could not read \`principals\` from the default branch (${why}) — falling back to vinaya's built-in principal allowlist.
|
|
3113
|
+
`);
|
|
3114
|
+
let base64;
|
|
3115
|
+
try {
|
|
3116
|
+
base64 = fetcher().trim();
|
|
3117
|
+
} catch (err) {
|
|
3118
|
+
if (!isMissingFileError(err))
|
|
3119
|
+
warn(firstLine(err));
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
if (!base64)
|
|
3123
|
+
return null;
|
|
3124
|
+
try {
|
|
3125
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
3126
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
3127
|
+
if (parsed.success)
|
|
3128
|
+
return parsed.data;
|
|
3129
|
+
warn("it did not satisfy the config schema");
|
|
3130
|
+
return null;
|
|
3131
|
+
} catch {
|
|
3132
|
+
warn("it was not readable as JSON");
|
|
3133
|
+
return null;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
|
|
2947
3137
|
// src/checks/bin/check-dispatch-readiness.ts
|
|
2948
3138
|
var CHECK_NAME = "dispatch-readiness";
|
|
2949
3139
|
var execFileAsync4 = promisify4(execFile4);
|
|
2950
3140
|
function git(args) {
|
|
2951
3141
|
try {
|
|
2952
|
-
return
|
|
3142
|
+
return execFileSync3("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
2953
3143
|
} catch {
|
|
2954
3144
|
return "";
|
|
2955
3145
|
}
|
|
@@ -3000,10 +3190,24 @@ function resolveEdge(id, taskById, factsByTaskId) {
|
|
|
3000
3190
|
const target = taskById.get(id);
|
|
3001
3191
|
if (target) {
|
|
3002
3192
|
const facts = target.issue !== null ? factsByTaskId.get(target.id) : undefined;
|
|
3003
|
-
return {
|
|
3193
|
+
return {
|
|
3194
|
+
issue: target.issue,
|
|
3195
|
+
merged: facts?.prState === "merged",
|
|
3196
|
+
open: facts?.prState === "open",
|
|
3197
|
+
issueState: facts?.issueState ?? null,
|
|
3198
|
+
stateReason: facts?.stateReason ?? null,
|
|
3199
|
+
closedByActor: facts?.closedByActor ?? null
|
|
3200
|
+
};
|
|
3004
3201
|
}
|
|
3005
3202
|
const direct = id.match(/^#(\d+)$/);
|
|
3006
|
-
return {
|
|
3203
|
+
return {
|
|
3204
|
+
issue: direct ? Number(direct[1]) : null,
|
|
3205
|
+
merged: false,
|
|
3206
|
+
open: false,
|
|
3207
|
+
issueState: null,
|
|
3208
|
+
stateReason: null,
|
|
3209
|
+
closedByActor: null
|
|
3210
|
+
};
|
|
3007
3211
|
}
|
|
3008
3212
|
async function main() {
|
|
3009
3213
|
const branch = currentBranch();
|
|
@@ -3046,7 +3250,14 @@ async function main() {
|
|
|
3046
3250
|
const factsByTaskId = snapshot.facts;
|
|
3047
3251
|
const dependsOn = task.dependsOn.map((dep) => {
|
|
3048
3252
|
const r = resolveEdge(dep, taskById, factsByTaskId);
|
|
3049
|
-
return {
|
|
3253
|
+
return {
|
|
3254
|
+
id: dep,
|
|
3255
|
+
issue: r.issue,
|
|
3256
|
+
merged: r.merged,
|
|
3257
|
+
issueState: r.issueState,
|
|
3258
|
+
stateReason: r.stateReason,
|
|
3259
|
+
closedByActor: r.closedByActor
|
|
3260
|
+
};
|
|
3050
3261
|
});
|
|
3051
3262
|
const conflictsWith = task.conflictsWith.map((c) => {
|
|
3052
3263
|
const r = resolveEdge(c, taskById, factsByTaskId);
|
|
@@ -3061,7 +3272,8 @@ async function main() {
|
|
|
3061
3272
|
dependsOn,
|
|
3062
3273
|
conflictsWith,
|
|
3063
3274
|
priorTask: null,
|
|
3064
|
-
priorTrancheArchival
|
|
3275
|
+
priorTrancheArchival,
|
|
3276
|
+
principalAllowlist: resolvePrincipalAllowlist(loadTrustAnchorConfig())
|
|
3065
3277
|
};
|
|
3066
3278
|
const result = checkDispatchReadiness(input);
|
|
3067
3279
|
if (!result.ready) {
|
|
@@ -1219,7 +1219,8 @@ function mapForgeFacts(raw) {
|
|
|
1219
1219
|
reviewDecision: mapReviewDecision(raw.pullRequest?.reviewDecision),
|
|
1220
1220
|
stateReason: mapStateReason(raw.issue.stateReason),
|
|
1221
1221
|
closedAt: raw.issue.closedAt ?? null,
|
|
1222
|
-
mergedAt: raw.pullRequest?.mergedAt ?? null
|
|
1222
|
+
mergedAt: raw.pullRequest?.mergedAt ?? null,
|
|
1223
|
+
closedByActor: raw.closedByActor ?? null
|
|
1223
1224
|
};
|
|
1224
1225
|
}
|
|
1225
1226
|
function mapStateReason(reason) {
|
|
@@ -1312,6 +1313,7 @@ function buildBatchQuery(tranche, tasks) {
|
|
|
1312
1313
|
timelineItems(last: 1, itemTypes: [CLOSED_EVENT]) {
|
|
1313
1314
|
nodes {
|
|
1314
1315
|
... on ClosedEvent {
|
|
1316
|
+
actor { login }
|
|
1315
1317
|
closer {
|
|
1316
1318
|
... on PullRequest {
|
|
1317
1319
|
number
|
|
@@ -1349,7 +1351,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1349
1351
|
const issue = repository[`${alias}_issue`];
|
|
1350
1352
|
const ref = repository[`${alias}_ref`];
|
|
1351
1353
|
const prs = repository[`${alias}_prs`];
|
|
1352
|
-
const
|
|
1354
|
+
const timelineNode = issue?.timelineItems?.nodes?.[0];
|
|
1355
|
+
const rawCloser = timelineNode?.closer ?? null;
|
|
1353
1356
|
const closingPr = rawCloser && typeof rawCloser.number === "number" && typeof rawCloser.url === "string" ? rawCloser : null;
|
|
1354
1357
|
const branchPr = prs && prs.nodes.length > 0 && prs.nodes[0] ? prs.nodes[0] : null;
|
|
1355
1358
|
return {
|
|
@@ -1361,7 +1364,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1361
1364
|
labels: issue.labels.nodes.map((n) => n.name)
|
|
1362
1365
|
} : null,
|
|
1363
1366
|
refExists: Boolean(ref && ref.name.length > 0),
|
|
1364
|
-
pullRequest: closingPr ?? branchPr
|
|
1367
|
+
pullRequest: closingPr ?? branchPr,
|
|
1368
|
+
closedByActor: timelineNode?.actor?.login ?? null
|
|
1365
1369
|
};
|
|
1366
1370
|
}
|
|
1367
1371
|
function describeError(err) {
|
|
@@ -1499,6 +1503,11 @@ var FORGE_FACT_INPUTS = [
|
|
|
1499
1503
|
fact: "mergedAt",
|
|
1500
1504
|
readsFrom: "PullRequest.mergedAt",
|
|
1501
1505
|
meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
fact: "closedByActor",
|
|
1509
|
+
readsFrom: "ClosedEvent.actor.login (most recent timeline CLOSED_EVENT)",
|
|
1510
|
+
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."
|
|
1502
1511
|
}
|
|
1503
1512
|
];
|
|
1504
1513
|
var DERIVED_STATUSES = [
|
|
@@ -1978,7 +1987,7 @@ function isGrandfathered(isoDate) {
|
|
|
1978
1987
|
return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM;
|
|
1979
1988
|
}
|
|
1980
1989
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
1981
|
-
function checkA1(entries) {
|
|
1990
|
+
function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
|
|
1982
1991
|
const failures = [];
|
|
1983
1992
|
for (const e of entries) {
|
|
1984
1993
|
if (!e.facts)
|
|
@@ -1986,6 +1995,9 @@ function checkA1(entries) {
|
|
|
1986
1995
|
if (e.facts.stateReason === "not_planned")
|
|
1987
1996
|
continue;
|
|
1988
1997
|
if (e.facts.issueState === "closed" && e.facts.prState !== "merged") {
|
|
1998
|
+
const handClosed = e.facts.stateReason === "completed" && isPrincipal(e.facts.closedByActor, principalAllowlist);
|
|
1999
|
+
if (handClosed)
|
|
2000
|
+
continue;
|
|
1989
2001
|
failures.push({
|
|
1990
2002
|
issue: e.task.issue,
|
|
1991
2003
|
tranche: e.trancheSlug,
|
|
@@ -2675,9 +2687,13 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
2675
2687
|
return terms;
|
|
2676
2688
|
}
|
|
2677
2689
|
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2690
|
+
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2691
|
+
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
2692
|
+
}
|
|
2678
2693
|
function checkDispatchReadiness(input) {
|
|
2679
2694
|
const { trancheSlug, task } = input;
|
|
2680
2695
|
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
2696
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2681
2697
|
const blockers = [];
|
|
2682
2698
|
if (task.issue === null) {
|
|
2683
2699
|
blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
|
|
@@ -2688,7 +2704,7 @@ function checkDispatchReadiness(input) {
|
|
|
2688
2704
|
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.`);
|
|
2689
2705
|
}
|
|
2690
2706
|
for (const dep of input.dependsOn) {
|
|
2691
|
-
if (!dep.merged) {
|
|
2707
|
+
if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
|
|
2692
2708
|
const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
|
|
2693
2709
|
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
|
|
2694
2710
|
}
|
|
@@ -1220,7 +1220,8 @@ function mapForgeFacts(raw) {
|
|
|
1220
1220
|
reviewDecision: mapReviewDecision(raw.pullRequest?.reviewDecision),
|
|
1221
1221
|
stateReason: mapStateReason(raw.issue.stateReason),
|
|
1222
1222
|
closedAt: raw.issue.closedAt ?? null,
|
|
1223
|
-
mergedAt: raw.pullRequest?.mergedAt ?? null
|
|
1223
|
+
mergedAt: raw.pullRequest?.mergedAt ?? null,
|
|
1224
|
+
closedByActor: raw.closedByActor ?? null
|
|
1224
1225
|
};
|
|
1225
1226
|
}
|
|
1226
1227
|
function mapStateReason(reason) {
|
|
@@ -1313,6 +1314,7 @@ function buildBatchQuery(tranche, tasks) {
|
|
|
1313
1314
|
timelineItems(last: 1, itemTypes: [CLOSED_EVENT]) {
|
|
1314
1315
|
nodes {
|
|
1315
1316
|
... on ClosedEvent {
|
|
1317
|
+
actor { login }
|
|
1316
1318
|
closer {
|
|
1317
1319
|
... on PullRequest {
|
|
1318
1320
|
number
|
|
@@ -1350,7 +1352,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1350
1352
|
const issue = repository[`${alias}_issue`];
|
|
1351
1353
|
const ref = repository[`${alias}_ref`];
|
|
1352
1354
|
const prs = repository[`${alias}_prs`];
|
|
1353
|
-
const
|
|
1355
|
+
const timelineNode = issue?.timelineItems?.nodes?.[0];
|
|
1356
|
+
const rawCloser = timelineNode?.closer ?? null;
|
|
1354
1357
|
const closingPr = rawCloser && typeof rawCloser.number === "number" && typeof rawCloser.url === "string" ? rawCloser : null;
|
|
1355
1358
|
const branchPr = prs && prs.nodes.length > 0 && prs.nodes[0] ? prs.nodes[0] : null;
|
|
1356
1359
|
return {
|
|
@@ -1362,7 +1365,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1362
1365
|
labels: issue.labels.nodes.map((n) => n.name)
|
|
1363
1366
|
} : null,
|
|
1364
1367
|
refExists: Boolean(ref && ref.name.length > 0),
|
|
1365
|
-
pullRequest: closingPr ?? branchPr
|
|
1368
|
+
pullRequest: closingPr ?? branchPr,
|
|
1369
|
+
closedByActor: timelineNode?.actor?.login ?? null
|
|
1366
1370
|
};
|
|
1367
1371
|
}
|
|
1368
1372
|
function describeError(err) {
|
|
@@ -1500,6 +1504,11 @@ var FORGE_FACT_INPUTS = [
|
|
|
1500
1504
|
fact: "mergedAt",
|
|
1501
1505
|
readsFrom: "PullRequest.mergedAt",
|
|
1502
1506
|
meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
fact: "closedByActor",
|
|
1510
|
+
readsFrom: "ClosedEvent.actor.login (most recent timeline CLOSED_EVENT)",
|
|
1511
|
+
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."
|
|
1503
1512
|
}
|
|
1504
1513
|
];
|
|
1505
1514
|
var DERIVED_STATUSES = [
|
|
@@ -1979,7 +1988,7 @@ function isGrandfathered(isoDate) {
|
|
|
1979
1988
|
return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM;
|
|
1980
1989
|
}
|
|
1981
1990
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
1982
|
-
function checkA1(entries) {
|
|
1991
|
+
function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
|
|
1983
1992
|
const failures = [];
|
|
1984
1993
|
for (const e of entries) {
|
|
1985
1994
|
if (!e.facts)
|
|
@@ -1987,6 +1996,9 @@ function checkA1(entries) {
|
|
|
1987
1996
|
if (e.facts.stateReason === "not_planned")
|
|
1988
1997
|
continue;
|
|
1989
1998
|
if (e.facts.issueState === "closed" && e.facts.prState !== "merged") {
|
|
1999
|
+
const handClosed = e.facts.stateReason === "completed" && isPrincipal(e.facts.closedByActor, principalAllowlist);
|
|
2000
|
+
if (handClosed)
|
|
2001
|
+
continue;
|
|
1990
2002
|
failures.push({
|
|
1991
2003
|
issue: e.task.issue,
|
|
1992
2004
|
tranche: e.trancheSlug,
|
|
@@ -2676,9 +2688,13 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
2676
2688
|
return terms;
|
|
2677
2689
|
}
|
|
2678
2690
|
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2691
|
+
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2692
|
+
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
2693
|
+
}
|
|
2679
2694
|
function checkDispatchReadiness(input) {
|
|
2680
2695
|
const { trancheSlug, task } = input;
|
|
2681
2696
|
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
2697
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2682
2698
|
const blockers = [];
|
|
2683
2699
|
if (task.issue === null) {
|
|
2684
2700
|
blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
|
|
@@ -2689,7 +2705,7 @@ function checkDispatchReadiness(input) {
|
|
|
2689
2705
|
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.`);
|
|
2690
2706
|
}
|
|
2691
2707
|
for (const dep of input.dependsOn) {
|
|
2692
|
-
if (!dep.merged) {
|
|
2708
|
+
if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
|
|
2693
2709
|
const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
|
|
2694
2710
|
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
|
|
2695
2711
|
}
|
|
@@ -3130,14 +3146,43 @@ function resolvePrBody() {
|
|
|
3130
3146
|
}
|
|
3131
3147
|
return "";
|
|
3132
3148
|
}
|
|
3133
|
-
function
|
|
3134
|
-
|
|
3135
|
-
|
|
3149
|
+
function fetchPrLabels(prNumber) {
|
|
3150
|
+
try {
|
|
3151
|
+
const out = execFileSync3("gh", ["pr", "view", String(prNumber), "--json", "labels"], {
|
|
3152
|
+
encoding: "utf8",
|
|
3153
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3154
|
+
});
|
|
3155
|
+
return JSON.parse(out).labels.map((l) => l.name);
|
|
3156
|
+
} catch {
|
|
3157
|
+
return null;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
function fetchWaiverLabelActor(prNumber, label2) {
|
|
3161
|
+
try {
|
|
3162
|
+
const out = execFileSync3("gh", ["api", `repos/{owner}/{repo}/issues/${prNumber}/timeline`, "--paginate"], {
|
|
3163
|
+
encoding: "utf8",
|
|
3164
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3165
|
+
});
|
|
3166
|
+
const events = JSON.parse(out);
|
|
3167
|
+
const matches = events.filter((e) => e.event === "labeled" && e.label?.name === label2);
|
|
3168
|
+
return matches[matches.length - 1]?.actor?.login ?? null;
|
|
3169
|
+
} catch {
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
function waiverActive() {
|
|
3174
|
+
const prNumberStr = process.env.PR_NUMBER;
|
|
3175
|
+
if (!prNumberStr)
|
|
3176
|
+
return false;
|
|
3177
|
+
const prNumber = Number(prNumberStr);
|
|
3178
|
+
const labels = fetchPrLabels(prNumber);
|
|
3179
|
+
if (!labels?.includes(WAIVER_LABEL))
|
|
3136
3180
|
return false;
|
|
3181
|
+
const labelActor = fetchWaiverLabelActor(prNumber, WAIVER_LABEL);
|
|
3137
3182
|
return isWaiverLabelActorVerified({
|
|
3138
3183
|
label: WAIVER_LABEL,
|
|
3139
3184
|
labels,
|
|
3140
|
-
labelActor
|
|
3185
|
+
labelActor,
|
|
3141
3186
|
principalAllowlist: resolvePrincipalAllowlist(loadTrustAnchorConfig())
|
|
3142
3187
|
});
|
|
3143
3188
|
}
|
|
@@ -3150,7 +3195,7 @@ function main() {
|
|
|
3150
3195
|
process.exit(0);
|
|
3151
3196
|
}
|
|
3152
3197
|
const content = existsSync(DOC_OWNERS_PATH) ? readFileSync(DOC_OWNERS_PATH, "utf8") : null;
|
|
3153
|
-
const result = evaluateC5(changed, content, resolvePrBody(), existsSync,
|
|
3198
|
+
const result = evaluateC5(changed, content, resolvePrBody(), existsSync, waiverActive());
|
|
3154
3199
|
if (result.errors.length > 0) {
|
|
3155
3200
|
for (const message of result.errors) {
|
|
3156
3201
|
emitCheckError({
|