@devinnn/docdrift 0.1.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 +147 -0
- package/dist/src/cli.js +51 -0
- package/dist/src/config/load.js +25 -0
- package/dist/src/config/schema.js +55 -0
- package/dist/src/config/validate.js +36 -0
- package/dist/src/detect/docsCheck.js +48 -0
- package/dist/src/detect/heuristics.js +44 -0
- package/dist/src/detect/index.js +92 -0
- package/dist/src/detect/openapi.js +123 -0
- package/dist/src/devin/prompts.js +55 -0
- package/dist/src/devin/schemas.js +99 -0
- package/dist/src/devin/v1.js +105 -0
- package/dist/src/evidence/bundle.js +81 -0
- package/dist/src/github/client.js +86 -0
- package/dist/src/index.js +375 -0
- package/dist/src/model/state.js +10 -0
- package/dist/src/model/types.js +2 -0
- package/dist/src/policy/confidence.js +31 -0
- package/dist/src/policy/engine.js +108 -0
- package/dist/src/policy/state.js +17 -0
- package/dist/src/utils/exec.js +24 -0
- package/dist/src/utils/fs.js +39 -0
- package/dist/src/utils/git.js +33 -0
- package/dist/src/utils/glob.js +21 -0
- package/dist/src/utils/hash.js +10 -0
- package/dist/src/utils/json.js +19 -0
- package/dist/src/utils/log.js +26 -0
- package/package.json +42 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAutogenPrompt = buildAutogenPrompt;
|
|
4
|
+
exports.buildConceptualPrompt = buildConceptualPrompt;
|
|
5
|
+
function attachmentBlock(attachmentUrls) {
|
|
6
|
+
return attachmentUrls.map((url, index) => `- ATTACHMENT ${index + 1}: ${url}`).join("\n");
|
|
7
|
+
}
|
|
8
|
+
function buildAutogenPrompt(input) {
|
|
9
|
+
return [
|
|
10
|
+
"You are Devin. Task: update API reference docs to match actual code/spec changes.",
|
|
11
|
+
"",
|
|
12
|
+
"EVIDENCE (attachments):",
|
|
13
|
+
attachmentBlock(input.attachmentUrls),
|
|
14
|
+
"",
|
|
15
|
+
"Rules (hard):",
|
|
16
|
+
`1) Only modify files under: ${input.allowlist.join(", ")}`,
|
|
17
|
+
"2) Make the smallest change that makes docs correct.",
|
|
18
|
+
"3) Run verification commands and record results:",
|
|
19
|
+
...input.verificationCommands.map((cmd) => ` - ${cmd}`),
|
|
20
|
+
"4) Open ONE pull request for this doc area with a clear title.",
|
|
21
|
+
"5) Keep a reviewer-friendly PR description: what changed, why docs were wrong, how to validate.",
|
|
22
|
+
"",
|
|
23
|
+
"Structured Output:",
|
|
24
|
+
"- Maintain structured output in the provided JSON schema.",
|
|
25
|
+
"- Update it immediately when you:",
|
|
26
|
+
" a) finish planning (filesToEdit, summary, confidence),",
|
|
27
|
+
" b) begin editing (status=EDITING),",
|
|
28
|
+
" c) start/finish verification (status=VERIFYING + verification.results),",
|
|
29
|
+
" d) open PR (status=OPENED_PR + pr.url),",
|
|
30
|
+
" e) get blocked (status=BLOCKED + questions),",
|
|
31
|
+
" f) complete (status=DONE).",
|
|
32
|
+
"",
|
|
33
|
+
`Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence.`
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function buildConceptualPrompt(input) {
|
|
37
|
+
return [
|
|
38
|
+
"You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes.",
|
|
39
|
+
"",
|
|
40
|
+
"EVIDENCE (attachments):",
|
|
41
|
+
attachmentBlock(input.attachmentUrls),
|
|
42
|
+
"",
|
|
43
|
+
"Rules (hard):",
|
|
44
|
+
`1) Only modify files under: ${input.allowlist.join(", ")}`,
|
|
45
|
+
"2) Do NOT invent product policy. If unclear, ask 2-3 targeted questions instead of guessing.",
|
|
46
|
+
`3) Prefer OPEN_ISSUE when confidence < ${input.confidenceThreshold.toFixed(2)}.`,
|
|
47
|
+
"4) If you open a PR, keep it extremely small and include a checklist of assumptions.",
|
|
48
|
+
"",
|
|
49
|
+
"Structured Output:",
|
|
50
|
+
"- Update at planning, editing, verifying, open-pr, blocked, done milestones.",
|
|
51
|
+
"- If blocked, fill blocked.questions with concrete, reviewer-actionable questions.",
|
|
52
|
+
"",
|
|
53
|
+
"Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text."
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PatchResultSchema = exports.PatchPlanSchema = void 0;
|
|
4
|
+
exports.PatchPlanSchema = {
|
|
5
|
+
type: "object",
|
|
6
|
+
additionalProperties: false,
|
|
7
|
+
required: [
|
|
8
|
+
"status",
|
|
9
|
+
"docArea",
|
|
10
|
+
"mode",
|
|
11
|
+
"confidence",
|
|
12
|
+
"summary",
|
|
13
|
+
"evidence",
|
|
14
|
+
"filesToEdit",
|
|
15
|
+
"verification",
|
|
16
|
+
"nextAction"
|
|
17
|
+
],
|
|
18
|
+
properties: {
|
|
19
|
+
status: { enum: ["PLANNING", "EDITING", "VERIFYING", "OPENED_PR", "BLOCKED", "DONE"] },
|
|
20
|
+
docArea: { type: "string" },
|
|
21
|
+
mode: { enum: ["autogen", "conceptual"] },
|
|
22
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
23
|
+
summary: { type: "string" },
|
|
24
|
+
evidence: {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
required: ["attachments", "diffSummary"],
|
|
28
|
+
properties: {
|
|
29
|
+
attachments: { type: "array", items: { type: "string" } },
|
|
30
|
+
diffSummary: { type: "string" }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
filesToEdit: { type: "array", items: { type: "string" } },
|
|
34
|
+
verification: {
|
|
35
|
+
type: "object",
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
required: ["commands"],
|
|
38
|
+
properties: {
|
|
39
|
+
commands: { type: "array", items: { type: "string" } },
|
|
40
|
+
results: { type: "array", items: { type: "string" } }
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
nextAction: { enum: ["OPEN_PR", "OPEN_ISSUE", "NOOP"] },
|
|
44
|
+
pr: {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
properties: {
|
|
48
|
+
title: { type: "string" },
|
|
49
|
+
url: { type: "string" }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
blocked: {
|
|
53
|
+
type: "object",
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
properties: {
|
|
56
|
+
reason: { type: "string" },
|
|
57
|
+
questions: { type: "array", items: { type: "string" } }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.PatchResultSchema = {
|
|
63
|
+
type: "object",
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
required: ["outcome", "confidence", "summary", "validation", "links"],
|
|
66
|
+
properties: {
|
|
67
|
+
outcome: { enum: ["PR_OPENED", "ISSUE_OPENED", "NO_CHANGE", "BLOCKED"] },
|
|
68
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
69
|
+
summary: { type: "string" },
|
|
70
|
+
changes: { type: "array", items: { type: "string" } },
|
|
71
|
+
validation: {
|
|
72
|
+
type: "object",
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ["commands", "results"],
|
|
75
|
+
properties: {
|
|
76
|
+
commands: { type: "array", items: { type: "string" } },
|
|
77
|
+
results: { type: "array", items: { type: "string" } }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
links: {
|
|
81
|
+
type: "object",
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
required: ["sessionUrl"],
|
|
84
|
+
properties: {
|
|
85
|
+
sessionUrl: { type: "string" },
|
|
86
|
+
prUrl: { type: "string" },
|
|
87
|
+
issueUrl: { type: "string" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
blocked: {
|
|
91
|
+
type: "object",
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
properties: {
|
|
94
|
+
reason: { type: "string" },
|
|
95
|
+
questions: { type: "array", items: { type: "string" } }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.devinUploadAttachment = devinUploadAttachment;
|
|
7
|
+
exports.devinCreateSession = devinCreateSession;
|
|
8
|
+
exports.devinGetSession = devinGetSession;
|
|
9
|
+
exports.devinListSessions = devinListSessions;
|
|
10
|
+
exports.pollUntilTerminal = pollUntilTerminal;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
function ensureOk(response, body, context) {
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
throw new Error(`${context} failed: ${response.status} ${body}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function devinUploadAttachment(apiKey, filePath) {
|
|
19
|
+
const resolved = node_path_1.default.resolve(filePath);
|
|
20
|
+
const bytes = node_fs_1.default.readFileSync(resolved);
|
|
21
|
+
const blob = new Blob([bytes]);
|
|
22
|
+
const form = new FormData();
|
|
23
|
+
form.append("file", blob, node_path_1.default.basename(filePath));
|
|
24
|
+
const response = await fetch("https://api.devin.ai/v1/attachments", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${apiKey}`
|
|
28
|
+
},
|
|
29
|
+
body: form
|
|
30
|
+
});
|
|
31
|
+
const text = await response.text();
|
|
32
|
+
ensureOk(response, text, "Upload attachment");
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(text);
|
|
35
|
+
if (typeof parsed === "string") {
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
if (parsed?.url && typeof parsed.url === "string") {
|
|
39
|
+
return parsed.url;
|
|
40
|
+
}
|
|
41
|
+
throw new Error("Unexpected attachment response payload");
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(`Unable to parse attachment response: ${String(error)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function devinCreateSession(apiKey, body) {
|
|
48
|
+
const response = await fetch("https://api.devin.ai/v1/sessions", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${apiKey}`,
|
|
52
|
+
"Content-Type": "application/json"
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(body)
|
|
55
|
+
});
|
|
56
|
+
const text = await response.text();
|
|
57
|
+
ensureOk(response, text, "Create session");
|
|
58
|
+
return JSON.parse(text);
|
|
59
|
+
}
|
|
60
|
+
async function devinGetSession(apiKey, sessionId) {
|
|
61
|
+
const response = await fetch(`https://api.devin.ai/v1/sessions/${sessionId}`, {
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Bearer ${apiKey}`
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
ensureOk(response, text, "Get session");
|
|
68
|
+
return JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
async function devinListSessions(apiKey, params = {}) {
|
|
71
|
+
const url = new URL("https://api.devin.ai/v1/sessions");
|
|
72
|
+
if (params.limit) {
|
|
73
|
+
url.searchParams.set("limit", String(params.limit));
|
|
74
|
+
}
|
|
75
|
+
if (params.tag) {
|
|
76
|
+
url.searchParams.set("tag", params.tag);
|
|
77
|
+
}
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${apiKey}`
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const text = await response.text();
|
|
84
|
+
ensureOk(response, text, "List sessions");
|
|
85
|
+
const parsed = JSON.parse(text);
|
|
86
|
+
if (Array.isArray(parsed)) {
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.sessions)) {
|
|
90
|
+
return parsed.sessions;
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
async function pollUntilTerminal(apiKey, sessionId, timeoutMs = 30 * 60_000) {
|
|
95
|
+
const started = Date.now();
|
|
96
|
+
while (Date.now() - started < timeoutMs) {
|
|
97
|
+
const session = await devinGetSession(apiKey, sessionId);
|
|
98
|
+
const status = String(session.status_enum ?? session.status ?? "UNKNOWN").toLowerCase();
|
|
99
|
+
if (["finished", "blocked", "error", "cancelled", "done", "complete"].includes(status)) {
|
|
100
|
+
return session;
|
|
101
|
+
}
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Session polling timed out for ${sessionId}`);
|
|
105
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildEvidenceBundle = buildEvidenceBundle;
|
|
7
|
+
exports.writeMetrics = writeMetrics;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const exec_1 = require("../utils/exec");
|
|
10
|
+
const fs_1 = require("../utils/fs");
|
|
11
|
+
function toSafeName(value) {
|
|
12
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
13
|
+
}
|
|
14
|
+
function resolveEvidencePath(filePath) {
|
|
15
|
+
if (node_path_1.default.isAbsolute(filePath)) {
|
|
16
|
+
return filePath;
|
|
17
|
+
}
|
|
18
|
+
return node_path_1.default.resolve(filePath);
|
|
19
|
+
}
|
|
20
|
+
async function buildEvidenceBundle(input) {
|
|
21
|
+
const root = node_path_1.default.resolve(input.evidenceRoot);
|
|
22
|
+
const area = toSafeName(input.item.docArea);
|
|
23
|
+
const bundleDir = node_path_1.default.join(root, area);
|
|
24
|
+
const evidenceDir = node_path_1.default.join(bundleDir, "evidence");
|
|
25
|
+
const docsDir = node_path_1.default.join(bundleDir, "impacted_docs");
|
|
26
|
+
(0, fs_1.ensureDir)(evidenceDir);
|
|
27
|
+
(0, fs_1.ensureDir)(docsDir);
|
|
28
|
+
const copiedEvidence = [];
|
|
29
|
+
for (const signal of input.item.signals) {
|
|
30
|
+
for (const evidencePath of signal.evidence) {
|
|
31
|
+
const src = resolveEvidencePath(evidencePath);
|
|
32
|
+
const fileName = toSafeName(node_path_1.default.basename(src));
|
|
33
|
+
const dest = node_path_1.default.join(evidenceDir, fileName);
|
|
34
|
+
if ((0, fs_1.copyIfExists)(src, dest)) {
|
|
35
|
+
copiedEvidence.push(node_path_1.default.relative(bundleDir, dest));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const copiedDocs = [];
|
|
40
|
+
for (const docPath of input.item.impactedDocs) {
|
|
41
|
+
const src = node_path_1.default.resolve(docPath);
|
|
42
|
+
const dest = node_path_1.default.join(docsDir, toSafeName(docPath));
|
|
43
|
+
if ((0, fs_1.copyIfExists)(src, dest)) {
|
|
44
|
+
copiedDocs.push(node_path_1.default.relative(bundleDir, dest));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const manifestPath = node_path_1.default.join(bundleDir, "manifest.json");
|
|
48
|
+
(0, fs_1.writeJsonFile)(manifestPath, {
|
|
49
|
+
run: {
|
|
50
|
+
runId: input.runInfo.runId,
|
|
51
|
+
repo: input.runInfo.repo,
|
|
52
|
+
baseSha: input.runInfo.baseSha,
|
|
53
|
+
headSha: input.runInfo.headSha,
|
|
54
|
+
trigger: input.runInfo.trigger,
|
|
55
|
+
timestamp: input.runInfo.timestamp
|
|
56
|
+
},
|
|
57
|
+
docArea: input.item.docArea,
|
|
58
|
+
mode: input.item.mode,
|
|
59
|
+
summary: input.item.summary,
|
|
60
|
+
signals: input.item.signals,
|
|
61
|
+
impactedDocs: input.item.impactedDocs,
|
|
62
|
+
copiedEvidence,
|
|
63
|
+
copiedDocs
|
|
64
|
+
});
|
|
65
|
+
const archivePath = `${bundleDir}.tar.gz`;
|
|
66
|
+
const parent = node_path_1.default.dirname(bundleDir);
|
|
67
|
+
const name = node_path_1.default.basename(bundleDir);
|
|
68
|
+
const tarResult = await (0, exec_1.execCommand)(`tar -czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(parent)} ${JSON.stringify(name)}`);
|
|
69
|
+
if (tarResult.exitCode !== 0) {
|
|
70
|
+
throw new Error(`Failed to create evidence archive: ${tarResult.stderr || tarResult.stdout}`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
bundleDir,
|
|
74
|
+
archivePath,
|
|
75
|
+
manifestPath,
|
|
76
|
+
attachmentPaths: [archivePath, manifestPath]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function writeMetrics(metrics) {
|
|
80
|
+
(0, fs_1.writeJsonFile)(node_path_1.default.resolve(".docdrift", "metrics.json"), metrics);
|
|
81
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postCommitComment = postCommitComment;
|
|
4
|
+
exports.createIssue = createIssue;
|
|
5
|
+
exports.renderRunComment = renderRunComment;
|
|
6
|
+
exports.renderBlockedIssueBody = renderBlockedIssueBody;
|
|
7
|
+
const rest_1 = require("@octokit/rest");
|
|
8
|
+
function parseRepo(full) {
|
|
9
|
+
const [owner, repo] = full.split("/");
|
|
10
|
+
if (!owner || !repo) {
|
|
11
|
+
throw new Error(`Invalid repository slug: ${full}`);
|
|
12
|
+
}
|
|
13
|
+
return { owner, repo };
|
|
14
|
+
}
|
|
15
|
+
async function postCommitComment(input) {
|
|
16
|
+
const octokit = new rest_1.Octokit({ auth: input.token });
|
|
17
|
+
const { owner, repo } = parseRepo(input.repository);
|
|
18
|
+
const response = await octokit.repos.createCommitComment({
|
|
19
|
+
owner,
|
|
20
|
+
repo,
|
|
21
|
+
commit_sha: input.commitSha,
|
|
22
|
+
body: input.body
|
|
23
|
+
});
|
|
24
|
+
return response.data.html_url;
|
|
25
|
+
}
|
|
26
|
+
async function createIssue(input) {
|
|
27
|
+
const octokit = new rest_1.Octokit({ auth: input.token });
|
|
28
|
+
const { owner, repo } = parseRepo(input.repository);
|
|
29
|
+
const response = await octokit.issues.create({
|
|
30
|
+
owner,
|
|
31
|
+
repo,
|
|
32
|
+
title: input.issue.title,
|
|
33
|
+
body: input.issue.body,
|
|
34
|
+
labels: input.issue.labels
|
|
35
|
+
});
|
|
36
|
+
return response.data.html_url;
|
|
37
|
+
}
|
|
38
|
+
function renderRunComment(input) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
lines.push(`## Doc Drift Result: ${input.docArea}`);
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(`- Decision: ${input.decision}`);
|
|
43
|
+
lines.push(`- Outcome: ${input.outcome}`);
|
|
44
|
+
lines.push(`- Summary: ${input.summary}`);
|
|
45
|
+
if (input.sessionUrl) {
|
|
46
|
+
lines.push(`- Devin Session: ${input.sessionUrl}`);
|
|
47
|
+
}
|
|
48
|
+
if (input.prUrl) {
|
|
49
|
+
lines.push(`- PR: ${input.prUrl}`);
|
|
50
|
+
}
|
|
51
|
+
if (input.issueUrl) {
|
|
52
|
+
lines.push(`- Issue: ${input.issueUrl}`);
|
|
53
|
+
}
|
|
54
|
+
if (input.validation.length) {
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push("### Validation");
|
|
57
|
+
for (const row of input.validation) {
|
|
58
|
+
lines.push(`- \`${row.command}\`: ${row.result}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
function renderBlockedIssueBody(input) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
lines.push(`Doc area: ${input.docArea}`);
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push("## Evidence");
|
|
68
|
+
lines.push(input.evidenceSummary);
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("## Questions");
|
|
71
|
+
for (const question of input.questions) {
|
|
72
|
+
lines.push(`- ${question}`);
|
|
73
|
+
}
|
|
74
|
+
if (input.suggestedPatch) {
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("## Suggested Patch");
|
|
77
|
+
lines.push("```diff");
|
|
78
|
+
lines.push(input.suggestedPatch);
|
|
79
|
+
lines.push("```");
|
|
80
|
+
}
|
|
81
|
+
if (input.sessionUrl) {
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push(`Session: ${input.sessionUrl}`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|