@deftai/directive-core 0.79.1 → 0.79.3
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/dist/cache/fetch.js +14 -4
- package/dist/capacity/backfill.js +4 -2
- package/dist/category-b-namespace/index.js +31 -6
- package/dist/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +9 -1
- package/dist/content-contracts/skills/skill-frontmatter.js +6 -2
- package/dist/eval/health.js +8 -1
- package/dist/eval/run.js +2 -0
- package/dist/fs/projection-containment.d.ts +11 -0
- package/dist/fs/projection-containment.js +62 -1
- package/dist/hooks/dispatcher.d.ts +1 -1
- package/dist/hooks/dispatcher.js +66 -7
- package/dist/init-deposit/hygiene.js +3 -0
- package/dist/intake/reconcile-issues.d.ts +1 -1
- package/dist/intake/reconcile-issues.js +25 -2
- package/dist/issue-sync/sync-from-xbrief-cli.d.ts +1 -0
- package/dist/issue-sync/sync-from-xbrief-cli.js +3 -0
- package/dist/issue-sync/sync-from-xbrief.d.ts +4 -0
- package/dist/issue-sync/sync-from-xbrief.js +12 -0
- package/dist/lifecycle/event.d.ts +15 -0
- package/dist/lifecycle/event.js +163 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/orchestration/verify-judgment-gates.js +18 -2
- package/dist/platform/resolve-version.d.ts +2 -6
- package/dist/platform/resolve-version.js +3 -129
- package/dist/pr-merge-readiness/compute.d.ts +2 -1
- package/dist/pr-merge-readiness/compute.js +23 -6
- package/dist/pr-merge-readiness/evaluate.d.ts +2 -1
- package/dist/pr-merge-readiness/evaluate.js +46 -31
- package/dist/pr-merge-readiness/gh.js +13 -5
- package/dist/pr-merge-readiness/greptile-inline.d.ts +27 -0
- package/dist/pr-merge-readiness/greptile-inline.js +239 -0
- package/dist/pr-merge-readiness/index.d.ts +1 -0
- package/dist/pr-merge-readiness/index.js +1 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +2 -1
- package/dist/pr-merge-readiness/mergeability.js +7 -1
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
- package/dist/pr-monitor/monitor.js +10 -1
- package/dist/pr-watch/constants.d.ts +5 -0
- package/dist/pr-watch/constants.js +27 -0
- package/dist/pr-watch/index.d.ts +1 -1
- package/dist/pr-watch/index.js +1 -1
- package/dist/pr-watch/main.d.ts +3 -0
- package/dist/pr-watch/main.js +29 -3
- package/dist/release/constants.d.ts +5 -0
- package/dist/release/constants.js +14 -2
- package/dist/release/flags.js +16 -0
- package/dist/release/main.js +7 -0
- package/dist/release/pipeline.js +7 -3
- package/dist/release/preflight.js +8 -1
- package/dist/release/skip-ci-incident.d.ts +22 -0
- package/dist/release/skip-ci-incident.js +68 -0
- package/dist/release/types.d.ts +2 -0
- package/dist/release/version.js +68 -32
- package/dist/release-e2e/entrypoint-worker.js +1 -0
- package/dist/release-e2e/entrypoint.js +13 -0
- package/dist/review-monitor/constants.d.ts +14 -0
- package/dist/review-monitor/constants.js +55 -0
- package/dist/review-monitor/index.d.ts +5 -0
- package/dist/review-monitor/index.js +5 -0
- package/dist/review-monitor/record.d.ts +50 -0
- package/dist/review-monitor/record.js +178 -0
- package/dist/review-monitor/tier-detection.d.ts +15 -0
- package/dist/review-monitor/tier-detection.js +61 -0
- package/dist/review-monitor/verify.d.ts +32 -0
- package/dist/review-monitor/verify.js +171 -0
- package/dist/scope/demote.js +20 -0
- package/dist/swarm/worktrees.d.ts +3 -0
- package/dist/swarm/worktrees.js +18 -2
- package/dist/task-surface/index.js +3 -2
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +16 -8
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +41 -14
- package/dist/xbrief-migrate/migration-containment.js +9 -25
- package/package.json +7 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { emit, main as eventsMain, readEvents } from "./events.js";
|
|
3
|
+
const APPROVAL_PHRASES = new Set(["yes", "confirmed", "approve", "other"]);
|
|
4
|
+
function parseBooleanFlag(value) {
|
|
5
|
+
return ["1", "true", "yes"].includes(value.toLowerCase());
|
|
6
|
+
}
|
|
7
|
+
function repositoryFromPlanRef(planRef) {
|
|
8
|
+
const match = /^https?:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/i.exec(planRef.trim());
|
|
9
|
+
return match?.[1];
|
|
10
|
+
}
|
|
11
|
+
function dedupeKey(payload) {
|
|
12
|
+
const planRef = payload.plan_ref;
|
|
13
|
+
const approver = payload.approver;
|
|
14
|
+
if (typeof planRef !== "string" || typeof approver !== "string") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const parts = [planRef, approver];
|
|
18
|
+
if (typeof payload.pr_number === "number") {
|
|
19
|
+
parts.push(String(payload.pr_number));
|
|
20
|
+
}
|
|
21
|
+
if (typeof payload.head_sha === "string" && payload.head_sha.length > 0) {
|
|
22
|
+
parts.push(payload.head_sha);
|
|
23
|
+
}
|
|
24
|
+
return parts.join("|");
|
|
25
|
+
}
|
|
26
|
+
function findExistingPlanApproved(key, logPath) {
|
|
27
|
+
for (const record of readEvents(logPath)) {
|
|
28
|
+
if (record.event !== "plan:approved") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (dedupeKey(record.payload) === key) {
|
|
32
|
+
return record;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function validatePlanApprovedPayload(payload) {
|
|
38
|
+
const phrase = payload.approval_phrase;
|
|
39
|
+
if (phrase === undefined) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (typeof phrase !== "string") {
|
|
43
|
+
throw new Error("approval_phrase must be a string");
|
|
44
|
+
}
|
|
45
|
+
const normalized = phrase.toLowerCase();
|
|
46
|
+
if (!APPROVAL_PHRASES.has(normalized)) {
|
|
47
|
+
throw new Error(`invalid approval_phrase '${phrase}'; expected one of yes, confirmed, approve, other`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Parse `emit <event> ...` argv for lifecycle:event approval recording. */
|
|
51
|
+
export function parseEmitInvocation(args) {
|
|
52
|
+
const payload = {};
|
|
53
|
+
let name;
|
|
54
|
+
let log;
|
|
55
|
+
let i = 0;
|
|
56
|
+
if (args.length > 0 && !args[0]?.startsWith("--")) {
|
|
57
|
+
name = args[0];
|
|
58
|
+
i = 1;
|
|
59
|
+
}
|
|
60
|
+
if (name === undefined) {
|
|
61
|
+
throw new Error("event name required");
|
|
62
|
+
}
|
|
63
|
+
while (i < args.length) {
|
|
64
|
+
const arg = args[i];
|
|
65
|
+
if (arg === "--payload") {
|
|
66
|
+
const raw = args[i + 1];
|
|
67
|
+
if (raw === undefined) {
|
|
68
|
+
throw new Error("--payload requires a value");
|
|
69
|
+
}
|
|
70
|
+
const data = JSON.parse(raw);
|
|
71
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
72
|
+
throw new Error("--payload must be a JSON object");
|
|
73
|
+
}
|
|
74
|
+
Object.assign(payload, data);
|
|
75
|
+
i += 2;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--log") {
|
|
79
|
+
log = args[i + 1];
|
|
80
|
+
i += 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const flagMap = {
|
|
84
|
+
"--plan-ref": "plan_ref",
|
|
85
|
+
"--approver": "approver",
|
|
86
|
+
"--approval-phrase": "approval_phrase",
|
|
87
|
+
"--head-sha": "head_sha",
|
|
88
|
+
};
|
|
89
|
+
if (arg === "--pr-number") {
|
|
90
|
+
payload.pr_number = Number.parseInt(args[i + 1] ?? "", 10);
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const field = flagMap[arg ?? ""];
|
|
95
|
+
if (field !== undefined) {
|
|
96
|
+
payload[field] = args[i + 1];
|
|
97
|
+
i += 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === "--inline") {
|
|
101
|
+
payload.inline = parseBooleanFlag(args[i + 1] ?? "");
|
|
102
|
+
i += 2;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`unrecognized arguments: ${arg}`);
|
|
106
|
+
}
|
|
107
|
+
return { name, payload, log };
|
|
108
|
+
}
|
|
109
|
+
function enrichPlanApprovedPayload(payload) {
|
|
110
|
+
const next = { ...payload };
|
|
111
|
+
if (typeof next.plan_ref === "string" && next.repository === undefined) {
|
|
112
|
+
const repository = repositoryFromPlanRef(next.plan_ref);
|
|
113
|
+
if (repository !== undefined) {
|
|
114
|
+
next.repository = repository;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
function emitPlanApprovedIdempotent(payload, options = {}) {
|
|
120
|
+
const enriched = enrichPlanApprovedPayload(payload);
|
|
121
|
+
validatePlanApprovedPayload(enriched);
|
|
122
|
+
const key = dedupeKey(enriched);
|
|
123
|
+
if (key !== null) {
|
|
124
|
+
const existing = findExistingPlanApproved(key, options.logPath ?? null);
|
|
125
|
+
if (existing !== null) {
|
|
126
|
+
return existing;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return emit("plan:approved", enriched, options);
|
|
130
|
+
}
|
|
131
|
+
/** CLI entry for `lifecycle:event` / review-cycle approval recorder (#2631). */
|
|
132
|
+
export function runLifecycleEvent(argv, options = {}) {
|
|
133
|
+
if (options.projectRoot !== undefined) {
|
|
134
|
+
process.chdir(resolve(options.projectRoot));
|
|
135
|
+
}
|
|
136
|
+
if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help") {
|
|
137
|
+
process.stderr.write("usage: lifecycle:event emit plan:approved --plan-ref <url> --approver <login> " +
|
|
138
|
+
"[--approval-phrase yes|confirmed|approve] [--pr-number N] [--head-sha SHA]\n");
|
|
139
|
+
return argv.length === 0 ? 2 : 0;
|
|
140
|
+
}
|
|
141
|
+
if (argv[0] !== "emit") {
|
|
142
|
+
return eventsMain(argv);
|
|
143
|
+
}
|
|
144
|
+
if (argv[1] !== "plan:approved") {
|
|
145
|
+
return eventsMain(argv);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const parsed = parseEmitInvocation(argv.slice(1));
|
|
149
|
+
const record = emitPlanApprovedIdempotent(parsed.payload, { logPath: parsed.log ?? null });
|
|
150
|
+
process.stdout.write(`${JSON.stringify(record)}\n`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
process.stderr.write(`lifecycle:event failed: ${message}\n`);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Process entrypoint mirroring other lifecycle CLI modules. */
|
|
160
|
+
export function main(argv = process.argv.slice(2)) {
|
|
161
|
+
return runLifecycleEvent(argv);
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=event.js.map
|
package/dist/lifecycle/index.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
7
7
|
import { join, resolve } from "node:path";
|
|
8
|
+
import { assertProjectionContained, assertWriteTargetSafe, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
8
9
|
import { resolveAuditDir } from "../layout/resolve.js";
|
|
9
10
|
import { resolveJudgmentGates } from "./judgment-policy.js";
|
|
10
11
|
import { matchAny } from "./pathspec.js";
|
|
@@ -163,7 +164,22 @@ export function readClearances(projectRoot, logPath) {
|
|
|
163
164
|
}
|
|
164
165
|
export function recordClearance(projectRoot, options) {
|
|
165
166
|
const path = options.log_path ?? clearanceLogPath(projectRoot);
|
|
166
|
-
|
|
167
|
+
const auditDir = resolveAuditDir(projectRoot);
|
|
168
|
+
assertProjectionContained(projectRoot, auditDir);
|
|
169
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
170
|
+
try {
|
|
171
|
+
const info = lstatSync(auditDir);
|
|
172
|
+
if (info.isSymbolicLink()) {
|
|
173
|
+
throw new ProjectionContainmentError(`projection write refused: audit directory ${auditDir} must not be a symlink`, { projectDir: resolve(projectRoot), targetPath: auditDir, offendingPath: auditDir });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
if (err instanceof ProjectionContainmentError) {
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
// Audit dir does not exist yet; mkdirSync below will create it under containment.
|
|
181
|
+
}
|
|
182
|
+
mkdirSync(auditDir, { recursive: true });
|
|
167
183
|
const entry = {
|
|
168
184
|
clearance_id: randomUUID(),
|
|
169
185
|
timestamp: utcNowIso(options.now),
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
+
import { isPublishable, NonPublishableVersionError, toPep440 } from "../release/version.js";
|
|
1
2
|
import { DEV_FALLBACK, ENV_VAR } from "./constants.js";
|
|
2
|
-
export { DEV_FALLBACK, ENV_VAR };
|
|
3
|
-
export declare class NonPublishableVersionError extends Error {
|
|
4
|
-
constructor(message: string);
|
|
5
|
-
}
|
|
6
|
-
export declare function toPep440(version: string): string;
|
|
7
|
-
export declare function isPublishable(version: string): boolean;
|
|
3
|
+
export { DEV_FALLBACK, ENV_VAR, isPublishable, NonPublishableVersionError, toPep440 };
|
|
8
4
|
export declare function tagNameFromRef(ref: string): string;
|
|
9
5
|
export declare function latestPublishableTag(tags: Iterable<string>): string | null;
|
|
10
6
|
export declare function payloadIsOwnGitRoot(payloadDir: string): boolean;
|
|
@@ -2,113 +2,15 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { comparePublishableVersions, isPublishable, NonPublishableVersionError, toPep440, } from "../release/version.js";
|
|
5
6
|
import { DEV_FALLBACK, ENV_VAR } from "./constants.js";
|
|
6
|
-
export { DEV_FALLBACK, ENV_VAR };
|
|
7
|
-
export class NonPublishableVersionError extends Error {
|
|
8
|
-
constructor(message) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = "NonPublishableVersionError";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
const PRE_KIND_MAP = {
|
|
14
|
-
alpha: "a",
|
|
15
|
-
beta: "b",
|
|
16
|
-
rc: "rc",
|
|
17
|
-
};
|
|
18
|
-
const NON_PUBLISHABLE_KINDS = new Set(["test"]);
|
|
19
|
-
const PRERELEASE_RANK = {
|
|
20
|
-
alpha: 0,
|
|
21
|
-
beta: 1,
|
|
22
|
-
rc: 2,
|
|
23
|
-
"": 3,
|
|
24
|
-
};
|
|
7
|
+
export { DEV_FALLBACK, ENV_VAR, isPublishable, NonPublishableVersionError, toPep440 };
|
|
25
8
|
function frameworkRoot() {
|
|
26
9
|
const envRoot = process.env.DEFT_ROOT?.trim();
|
|
27
10
|
if (envRoot)
|
|
28
11
|
return resolve(envRoot);
|
|
29
12
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
30
13
|
}
|
|
31
|
-
/** Parse `[v]X.Y.Z[-(rc|alpha|beta|test).N]` via linear scan. */
|
|
32
|
-
function parsePep440Tag(version) {
|
|
33
|
-
let i = 0;
|
|
34
|
-
if (version[i] === "v" || version[i] === "V")
|
|
35
|
-
i += 1;
|
|
36
|
-
const readInt = () => {
|
|
37
|
-
const ch = version[i] ?? "";
|
|
38
|
-
if (i >= version.length || ch < "0" || ch > "9")
|
|
39
|
-
return null;
|
|
40
|
-
let n = 0;
|
|
41
|
-
while (i < version.length) {
|
|
42
|
-
const digit = version[i] ?? "";
|
|
43
|
-
if (digit < "0" || digit > "9")
|
|
44
|
-
break;
|
|
45
|
-
n = n * 10 + Number(digit);
|
|
46
|
-
i += 1;
|
|
47
|
-
}
|
|
48
|
-
return n;
|
|
49
|
-
};
|
|
50
|
-
const major = readInt();
|
|
51
|
-
if (major === null || version[i] !== ".")
|
|
52
|
-
return null;
|
|
53
|
-
i += 1;
|
|
54
|
-
const minor = readInt();
|
|
55
|
-
if (minor === null || version[i] !== ".")
|
|
56
|
-
return null;
|
|
57
|
-
i += 1;
|
|
58
|
-
const patch = readInt();
|
|
59
|
-
if (patch === null)
|
|
60
|
-
return null;
|
|
61
|
-
if (i >= version.length)
|
|
62
|
-
return { major, minor, patch, kind: null, num: null };
|
|
63
|
-
if (version[i] !== "-")
|
|
64
|
-
return null;
|
|
65
|
-
i += 1;
|
|
66
|
-
const kindStart = i;
|
|
67
|
-
while (i < version.length && version[i] !== ".")
|
|
68
|
-
i += 1;
|
|
69
|
-
if (i >= version.length || version[i] !== ".")
|
|
70
|
-
return null;
|
|
71
|
-
const kind = version.slice(kindStart, i);
|
|
72
|
-
if (!["rc", "alpha", "beta", "test"].includes(kind))
|
|
73
|
-
return null;
|
|
74
|
-
i += 1;
|
|
75
|
-
const num = readInt();
|
|
76
|
-
if (num === null || i !== version.length)
|
|
77
|
-
return null;
|
|
78
|
-
return { major, minor, patch, kind, num };
|
|
79
|
-
}
|
|
80
|
-
export function toPep440(version) {
|
|
81
|
-
if (typeof version !== "string") {
|
|
82
|
-
throw new Error(`version must be a string, got ${typeof version}`);
|
|
83
|
-
}
|
|
84
|
-
const candidate = version.trim();
|
|
85
|
-
if (!candidate)
|
|
86
|
-
throw new Error("version must be a non-empty string");
|
|
87
|
-
const parsed = parsePep440Tag(candidate);
|
|
88
|
-
if (parsed === null) {
|
|
89
|
-
throw new Error(`Cannot normalize ${JSON.stringify(candidate)} to PEP 440: expected [v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta|test).N`);
|
|
90
|
-
}
|
|
91
|
-
const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
92
|
-
if (parsed.kind === null)
|
|
93
|
-
return base;
|
|
94
|
-
if (NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
|
|
95
|
-
throw new NonPublishableVersionError(`Version ${JSON.stringify(candidate)} carries non-publishable pre-release tag ${JSON.stringify(parsed.kind)}.${parsed.num} -- release pipeline MUST skip pyproject.toml [project].version sync for this tag.`);
|
|
96
|
-
}
|
|
97
|
-
const pepKind = PRE_KIND_MAP[parsed.kind];
|
|
98
|
-
if (pepKind === undefined) {
|
|
99
|
-
throw new Error(`Unmapped pre-release kind ${JSON.stringify(parsed.kind)} for version ${JSON.stringify(candidate)}; add it to PRE_KIND_MAP or NON_PUBLISHABLE_KINDS to keep parser in lockstep with the publishability classifier.`);
|
|
100
|
-
}
|
|
101
|
-
return `${base}${pepKind}${parsed.num}`;
|
|
102
|
-
}
|
|
103
|
-
export function isPublishable(version) {
|
|
104
|
-
try {
|
|
105
|
-
toPep440(version);
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
14
|
export function tagNameFromRef(ref) {
|
|
113
15
|
let candidate = ref.trim();
|
|
114
16
|
if (!candidate)
|
|
@@ -123,49 +25,21 @@ export function tagNameFromRef(ref) {
|
|
|
123
25
|
candidate = candidate.slice(prefix.length);
|
|
124
26
|
return candidate.trim();
|
|
125
27
|
}
|
|
126
|
-
function publishableTagSortKey(version) {
|
|
127
|
-
const candidate = version.trim();
|
|
128
|
-
const parsed = parsePep440Tag(candidate);
|
|
129
|
-
if (parsed === null) {
|
|
130
|
-
throw new Error(`Cannot sort ${JSON.stringify(candidate)}: expected [v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N`);
|
|
131
|
-
}
|
|
132
|
-
if (parsed.kind !== null && NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
|
|
133
|
-
throw new NonPublishableVersionError(`Version ${JSON.stringify(candidate)} carries non-publishable pre-release tag ${JSON.stringify(parsed.kind)}.`);
|
|
134
|
-
}
|
|
135
|
-
const kind = parsed.kind ?? "";
|
|
136
|
-
const prereleaseRank = PRERELEASE_RANK[kind];
|
|
137
|
-
if (prereleaseRank === undefined) {
|
|
138
|
-
throw new Error(`Unmapped pre-release kind ${JSON.stringify(parsed.kind)} for ${JSON.stringify(candidate)}`);
|
|
139
|
-
}
|
|
140
|
-
return [parsed.major, parsed.minor, parsed.patch, prereleaseRank, parsed.num ?? 0];
|
|
141
|
-
}
|
|
142
28
|
export function latestPublishableTag(tags) {
|
|
143
29
|
let bestTag = null;
|
|
144
|
-
let bestKey = null;
|
|
145
30
|
for (const raw of tags) {
|
|
146
31
|
const tag = tagNameFromRef(raw);
|
|
147
32
|
if (!tag || !isPublishable(tag))
|
|
148
33
|
continue;
|
|
149
34
|
try {
|
|
150
|
-
|
|
151
|
-
if (bestKey === null || compareTuple(key, bestKey) > 0) {
|
|
35
|
+
if (bestTag === null || comparePublishableVersions(tag, bestTag) > 0) {
|
|
152
36
|
bestTag = tag;
|
|
153
|
-
bestKey = key;
|
|
154
37
|
}
|
|
155
38
|
}
|
|
156
39
|
catch { }
|
|
157
40
|
}
|
|
158
41
|
return bestTag;
|
|
159
42
|
}
|
|
160
|
-
function compareTuple(a, b) {
|
|
161
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
162
|
-
const av = a[i] ?? 0;
|
|
163
|
-
const bv = b[i] ?? 0;
|
|
164
|
-
if (av !== bv)
|
|
165
|
-
return av > bv ? 1 : -1;
|
|
166
|
-
}
|
|
167
|
-
return 0;
|
|
168
|
-
}
|
|
169
43
|
function readManifestTag(baseDir) {
|
|
170
44
|
const manifest = join(baseDir, "VERSION");
|
|
171
45
|
try {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { CiGateOptions } from "./ci-gate.js";
|
|
2
2
|
import { isMergeReady } from "./evaluate.js";
|
|
3
|
+
import { type InlineGreptileFindings } from "./greptile-inline.js";
|
|
3
4
|
import { type MergeabilitySignal } from "./mergeability.js";
|
|
4
5
|
import type { SlizardGateOptions } from "./slizard-gate.js";
|
|
5
6
|
import type { GateResult, RunGhFn } from "./types.js";
|
|
6
|
-
declare function buildGateResult(prNumber: number, repo: string | null, headSha: string | null, body: string, via: string, partialData?: Record<string, unknown>, error?: string | null): GateResult;
|
|
7
|
+
declare function buildGateResult(prNumber: number, repo: string | null, headSha: string | null, body: string, via: string, partialData?: Record<string, unknown>, error?: string | null, inline?: InlineGreptileFindings | null): GateResult;
|
|
7
8
|
/** Injectable GitHub-mergeability read seam (#2260) — keeps unit tests hermetic. */
|
|
8
9
|
export type FetchMergeabilityFn = (prNumber: number, repo: string, runGh: RunGhFn) => MergeabilitySignal;
|
|
9
10
|
export interface ComputeGateOptions extends CiGateOptions, SlizardGateOptions {
|
|
@@ -2,12 +2,13 @@ import { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
|
|
|
2
2
|
import { FALLBACK2_NOT_CLEAN_MSG, VIA_ERROR, VIA_FALLBACK1, VIA_FALLBACK2, VIA_PRIMARY, } from "./constants.js";
|
|
3
3
|
import { evaluateGates, isMergeReady } from "./evaluate.js";
|
|
4
4
|
import { fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrHeadSha, fetchPrHeadShaRest, resolveRepo, } from "./gh.js";
|
|
5
|
+
import { fetchUnresolvedGreptileInlineFindings, inlineFindingsToDict, } from "./greptile-inline.js";
|
|
5
6
|
import { fetchMergeability, isGithubMergeableClean, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
|
|
6
7
|
import { emptyVerdict, parseGreptileBody } from "./parse.js";
|
|
7
8
|
import { evaluateSlizardGate, isSlizardCheck } from "./slizard-gate.js";
|
|
8
|
-
function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, error = null) {
|
|
9
|
+
function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, error = null, inline = null) {
|
|
9
10
|
const verdict = parseGreptileBody(body);
|
|
10
|
-
const failures = evaluateGates(prNumber, headSha, verdict);
|
|
11
|
+
const failures = evaluateGates(prNumber, headSha, verdict, inline);
|
|
11
12
|
return {
|
|
12
13
|
prNumber,
|
|
13
14
|
repo,
|
|
@@ -92,10 +93,24 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
|
|
|
92
93
|
* is only soft-blocked -- absent or stale for the current head) reconcile
|
|
93
94
|
* against GitHub's own mergeability (#2260). Shared by primary + fallback1.
|
|
94
95
|
*/
|
|
96
|
+
function loadInlineGreptileFindings(prNumber, repo, headSha, runGh) {
|
|
97
|
+
const resolved = resolveRepo(repo, runGh);
|
|
98
|
+
if (resolved.repo === null) {
|
|
99
|
+
return {
|
|
100
|
+
p0Count: 0,
|
|
101
|
+
p1Count: 0,
|
|
102
|
+
unresolvedThreadCount: 0,
|
|
103
|
+
error: resolved.error || "repo unresolved for inline reviewThreads lookup; pass --repo OWNER/REPO",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return fetchUnresolvedGreptileInlineFindings(prNumber, resolved.repo, headSha, runGh);
|
|
107
|
+
}
|
|
95
108
|
function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
|
|
96
|
-
const failures = evaluateGates(prNumber, headSha, verdict);
|
|
97
109
|
const partialData = {};
|
|
98
110
|
const resolved = resolveRepo(repo, runGh);
|
|
111
|
+
const inline = loadInlineGreptileFindings(prNumber, repo, headSha, runGh);
|
|
112
|
+
partialData.greptile_inline = inlineFindingsToDict(inline);
|
|
113
|
+
const failures = evaluateGates(prNumber, headSha, verdict, inline);
|
|
99
114
|
if (failures.length === 0) {
|
|
100
115
|
const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
|
|
101
116
|
failures.push(...ci.failures);
|
|
@@ -108,7 +123,7 @@ function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
|
|
|
108
123
|
// blocking. Only reconcile a SOFT block (verdict absent / stale head SHA).
|
|
109
124
|
if (options.disableMergeabilityReconcile === true ||
|
|
110
125
|
resolved.repo === null ||
|
|
111
|
-
!verdictBlockIsSoftOnly(verdict, headSha)) {
|
|
126
|
+
!verdictBlockIsSoftOnly(verdict, headSha, inline)) {
|
|
112
127
|
return { failures, partialData };
|
|
113
128
|
}
|
|
114
129
|
const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
|
|
@@ -143,17 +158,19 @@ function computePrimary(prNumber, repo, runGh, options) {
|
|
|
143
158
|
return { result: null, partial };
|
|
144
159
|
}
|
|
145
160
|
partial.head_sha = headSha;
|
|
161
|
+
const resolved = resolveRepo(repo, runGh);
|
|
162
|
+
const effectiveRepo = resolved.repo ?? repo;
|
|
146
163
|
const body = fetchGreptileCommentBody(prNumber, repo, runGh);
|
|
147
164
|
if (body === null) {
|
|
148
165
|
partial.primary_error = "gh api /issues/<N>/comments --jq returned non-zero";
|
|
149
166
|
return { result: null, partial };
|
|
150
167
|
}
|
|
151
168
|
const verdict = parseGreptileBody(body);
|
|
152
|
-
const { failures, partialData } = finalizeVerdictGate(prNumber,
|
|
169
|
+
const { failures, partialData } = finalizeVerdictGate(prNumber, effectiveRepo, headSha, verdict, runGh, options);
|
|
153
170
|
return {
|
|
154
171
|
result: {
|
|
155
172
|
prNumber,
|
|
156
|
-
repo,
|
|
173
|
+
repo: effectiveRepo,
|
|
157
174
|
headSha,
|
|
158
175
|
verdict,
|
|
159
176
|
failures,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { InlineGreptileFindings } from "./greptile-inline.js";
|
|
1
2
|
import type { GreptileVerdict } from "./types.js";
|
|
2
3
|
/** Return failure messages (empty list == merge-ready). */
|
|
3
|
-
export declare function evaluateGates(_prNumber: number, headSha: string | null, verdict: GreptileVerdict): string[];
|
|
4
|
+
export declare function evaluateGates(_prNumber: number, headSha: string | null, verdict: GreptileVerdict, inline?: InlineGreptileFindings | null): string[];
|
|
4
5
|
export declare function isMergeReady(failures: readonly string[]): boolean;
|
|
5
6
|
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { INFORMAL_CLEAN_DIAGNOSTIC } from "./constants.js";
|
|
2
2
|
/** Return failure messages (empty list == merge-ready). */
|
|
3
|
-
export function evaluateGates(_prNumber, headSha, verdict) {
|
|
3
|
+
export function evaluateGates(_prNumber, headSha, verdict, inline = null) {
|
|
4
4
|
const failures = [];
|
|
5
5
|
if (!verdict.found) {
|
|
6
6
|
failures.push("No Greptile rolling-summary comment found on the PR. " +
|
|
@@ -8,40 +8,55 @@ export function evaluateGates(_prNumber, headSha, verdict) {
|
|
|
8
8
|
"Wait for the review to land before merging (see #796 late-bot-review re-check).");
|
|
9
9
|
return failures;
|
|
10
10
|
}
|
|
11
|
-
if (verdict.excludedAuthor) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
if (!verdict.excludedAuthor) {
|
|
12
|
+
if (verdict.errored) {
|
|
13
|
+
failures.push("Greptile review is in the ERRORED state on the current HEAD (#526). " +
|
|
14
|
+
"Retry via @greptileai or escalate per " +
|
|
15
|
+
"skills/deft-directive-swarm/SKILL.md Phase 6 Step 1.");
|
|
16
|
+
}
|
|
17
|
+
if (verdict.informalClean) {
|
|
18
|
+
failures.push(INFORMAL_CLEAN_DIAGNOSTIC);
|
|
19
|
+
return appendInlineFailures(failures, inline);
|
|
20
|
+
}
|
|
21
|
+
if (verdict.lastReviewedSha === null) {
|
|
22
|
+
failures.push("Could not parse `Last reviewed commit:` from Greptile body. " +
|
|
23
|
+
"The comment may be malformed or Greptile may still be writing it -- re-fetch.");
|
|
24
|
+
}
|
|
25
|
+
else if (headSha &&
|
|
26
|
+
!(headSha.startsWith(verdict.lastReviewedSha) || verdict.lastReviewedSha.startsWith(headSha))) {
|
|
27
|
+
failures.push(`Greptile last reviewed ${verdict.lastReviewedSha} but PR HEAD is ${headSha}. ` +
|
|
28
|
+
"Review is stale -- wait for Greptile to re-review the latest commit.");
|
|
29
|
+
}
|
|
30
|
+
if (verdict.confidence === null) {
|
|
31
|
+
failures.push("Could not parse `Confidence Score: X/5` from Greptile body. " +
|
|
32
|
+
"Confidence is a required exit-condition input per " +
|
|
33
|
+
"skills/deft-directive-review-cycle/SKILL.md Phase 2 Step 6.");
|
|
34
|
+
}
|
|
35
|
+
else if (verdict.confidence <= 3) {
|
|
36
|
+
failures.push(`Greptile confidence is ${verdict.confidence}/5; exit condition requires > 3. ` +
|
|
37
|
+
"Address remaining findings or push clarifying changes.");
|
|
38
|
+
}
|
|
39
|
+
if (verdict.p0Count > 0 || verdict.p1Count > 0) {
|
|
40
|
+
failures.push(`Greptile reports ${verdict.p0Count} P0 and ${verdict.p1Count} P1 findings ` +
|
|
41
|
+
"on the current HEAD. All P0 / P1 findings MUST be addressed before merge " +
|
|
42
|
+
"(P2 findings are non-blocking).");
|
|
43
|
+
}
|
|
18
44
|
}
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
return appendInlineFailures(failures, inline);
|
|
46
|
+
}
|
|
47
|
+
function appendInlineFailures(failures, inline) {
|
|
48
|
+
if (inline === null) {
|
|
21
49
|
return failures;
|
|
22
50
|
}
|
|
23
|
-
if (
|
|
24
|
-
failures.push("Could not
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
else if (headSha &&
|
|
28
|
-
!(headSha.startsWith(verdict.lastReviewedSha) || verdict.lastReviewedSha.startsWith(headSha))) {
|
|
29
|
-
failures.push(`Greptile last reviewed ${verdict.lastReviewedSha} but PR HEAD is ${headSha}. ` +
|
|
30
|
-
"Review is stale -- wait for Greptile to re-review the latest commit.");
|
|
31
|
-
}
|
|
32
|
-
if (verdict.confidence === null) {
|
|
33
|
-
failures.push("Could not parse `Confidence Score: X/5` from Greptile body. " +
|
|
34
|
-
"Confidence is a required exit-condition input per " +
|
|
35
|
-
"skills/deft-directive-review-cycle/SKILL.md Phase 2 Step 6.");
|
|
36
|
-
}
|
|
37
|
-
else if (verdict.confidence <= 3) {
|
|
38
|
-
failures.push(`Greptile confidence is ${verdict.confidence}/5; exit condition requires > 3. ` +
|
|
39
|
-
"Address remaining findings or push clarifying changes.");
|
|
51
|
+
if (inline.error !== null) {
|
|
52
|
+
failures.push("Could not verify Greptile inline review comments on the current HEAD (#2620). " +
|
|
53
|
+
`Root cause: ${inline.error}`);
|
|
40
54
|
}
|
|
41
|
-
if (
|
|
42
|
-
failures.push(`Greptile
|
|
43
|
-
|
|
44
|
-
"
|
|
55
|
+
else if (inline.p0Count > 0 || inline.p1Count > 0) {
|
|
56
|
+
failures.push(`Greptile has ${inline.p0Count} unresolved inline P0 and ${inline.p1Count} unresolved inline P1 ` +
|
|
57
|
+
`review comment(s) on the current HEAD across ${inline.unresolvedThreadCount} open thread(s). ` +
|
|
58
|
+
"Resolve or address all blocking inline threads before merge -- rolling-summary badge counts alone " +
|
|
59
|
+
"are insufficient (#2620).");
|
|
45
60
|
}
|
|
46
61
|
return failures;
|
|
47
62
|
}
|
|
@@ -66,7 +66,10 @@ export function fetchGreptileCommentBody(prNumber, repo, runGh) {
|
|
|
66
66
|
`repos/${resolvedRepo}/issues/${prNumber}/comments`,
|
|
67
67
|
"--paginate",
|
|
68
68
|
"--jq",
|
|
69
|
-
|
|
69
|
+
// Prefer most recently *updated* Greptile summary. Greptile edits the
|
|
70
|
+
// primary rolling summary in place; a later-created duplicate can stay
|
|
71
|
+
// SHA-stale and must not win over the refreshed primary (#1056 / #2658).
|
|
72
|
+
`[.[] | select(.user.login == "${GREPTILE_LOGIN}")] | sort_by(.updated_at) | last | .body // ""`,
|
|
70
73
|
];
|
|
71
74
|
const { returncode, stdout, stderr } = runGh(cmd);
|
|
72
75
|
if (returncode !== 0) {
|
|
@@ -123,7 +126,8 @@ function parsePaginatedComments(text) {
|
|
|
123
126
|
}
|
|
124
127
|
idx = end;
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
let bestBody = "";
|
|
130
|
+
let bestUpdatedAt = "";
|
|
127
131
|
for (const c of comments) {
|
|
128
132
|
if (c !== null &&
|
|
129
133
|
typeof c === "object" &&
|
|
@@ -136,13 +140,17 @@ function parsePaginatedComments(text) {
|
|
|
136
140
|
c.user.login === GREPTILE_LOGIN &&
|
|
137
141
|
"body" in c &&
|
|
138
142
|
typeof c.body === "string") {
|
|
139
|
-
|
|
143
|
+
const updatedAt = "updated_at" in c && typeof c.updated_at === "string" ? c.updated_at : "";
|
|
144
|
+
if (updatedAt >= bestUpdatedAt) {
|
|
145
|
+
bestUpdatedAt = updatedAt;
|
|
146
|
+
bestBody = c.body;
|
|
147
|
+
}
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
|
-
if (
|
|
150
|
+
if (bestBody.length === 0 && bestUpdatedAt.length === 0) {
|
|
143
151
|
return { body: "", error: "" };
|
|
144
152
|
}
|
|
145
|
-
return { body:
|
|
153
|
+
return { body: bestBody, error: "" };
|
|
146
154
|
}
|
|
147
155
|
/** Mirrors Python json.JSONDecoder.raw_decode for concatenated paginate arrays. */
|
|
148
156
|
class PaginatedJsonDecoder {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RunGhFn } from "./types.js";
|
|
2
|
+
/** Unresolved Greptile inline P0/P1 on the current PR HEAD (#2620). */
|
|
3
|
+
export interface InlineGreptileFindings {
|
|
4
|
+
readonly p0Count: number;
|
|
5
|
+
readonly p1Count: number;
|
|
6
|
+
readonly unresolvedThreadCount: number;
|
|
7
|
+
readonly error: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface InlineReviewComment {
|
|
10
|
+
readonly authorLogin: string;
|
|
11
|
+
readonly body: string;
|
|
12
|
+
readonly path: string | null;
|
|
13
|
+
readonly commitOid: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface InlineReviewThread {
|
|
16
|
+
readonly isResolved: boolean;
|
|
17
|
+
readonly isOutdated: boolean;
|
|
18
|
+
readonly comments: readonly InlineReviewComment[];
|
|
19
|
+
}
|
|
20
|
+
/** True when a review comment commit SHA matches the current PR head (#2620 AC-2). */
|
|
21
|
+
export declare function headShaMatches(commentSha: string, headSha: string): boolean;
|
|
22
|
+
export declare function inlineFindingsToDict(findings: InlineGreptileFindings): Record<string, unknown>;
|
|
23
|
+
/** Score unresolved Greptile inline P0/P1 threads pinned to the current HEAD (#2620). */
|
|
24
|
+
export declare function evaluateInlineReviewThreads(threads: readonly InlineReviewThread[], headSha: string): InlineGreptileFindings;
|
|
25
|
+
/** Fetch unresolved Greptile inline P0/P1 on the current HEAD via reviewThreads GraphQL (#2620). */
|
|
26
|
+
export declare function fetchUnresolvedGreptileInlineFindings(prNumber: number, repo: string, headSha: string, runGh: RunGhFn): InlineGreptileFindings;
|
|
27
|
+
//# sourceMappingURL=greptile-inline.d.ts.map
|