@devinnn/docdrift 0.1.0 → 0.1.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.
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.detectOpenApiDrift = detectOpenApiDrift;
7
+ exports.detectOpenApiDriftFromNormalized = detectOpenApiDriftFromNormalized;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const exec_1 = require("../utils/exec");
@@ -56,7 +57,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
56
57
  "\n--- stdout ---",
57
58
  exportResult.stdout,
58
59
  "\n--- stderr ---",
59
- exportResult.stderr
60
+ exportResult.stderr,
60
61
  ].join("\n"), "utf8");
61
62
  if (exportResult.exitCode !== 0) {
62
63
  return {
@@ -67,8 +68,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
67
68
  kind: "weak_evidence",
68
69
  tier: 2,
69
70
  confidence: 0.35,
70
- evidence: [exportLogPath]
71
- }
71
+ evidence: [exportLogPath],
72
+ },
72
73
  };
73
74
  }
74
75
  if (!node_fs_1.default.existsSync(openapi.generatedPath) || !node_fs_1.default.existsSync(openapi.publishedPath)) {
@@ -80,8 +81,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
80
81
  kind: "weak_evidence",
81
82
  tier: 2,
82
83
  confidence: 0.35,
83
- evidence: [exportLogPath]
84
- }
84
+ evidence: [exportLogPath],
85
+ },
85
86
  };
86
87
  }
87
88
  const generatedRaw = node_fs_1.default.readFileSync(openapi.generatedPath, "utf8");
@@ -94,7 +95,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
94
95
  return {
95
96
  impactedDocs: [openapi.publishedPath],
96
97
  evidenceFiles: [exportLogPath],
97
- summary: "No OpenAPI drift detected"
98
+ summary: "No OpenAPI drift detected",
98
99
  };
99
100
  }
100
101
  const summary = summarizeSpecDelta(publishedJson, generatedJson);
@@ -107,7 +108,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
107
108
  normalizedPublished,
108
109
  "",
109
110
  "# Generated (normalized)",
110
- normalizedGenerated
111
+ normalizedGenerated,
111
112
  ].join("\n"), "utf8");
112
113
  return {
113
114
  impactedDocs: [...new Set([openapi.publishedPath, ...(docArea.patch.targets ?? [])])],
@@ -117,7 +118,89 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
117
118
  kind: "openapi_diff",
118
119
  tier: 1,
119
120
  confidence: 0.95,
120
- evidence: [diffPath]
121
- }
121
+ evidence: [diffPath],
122
+ },
123
+ };
124
+ }
125
+ /** Run OpenAPI drift detection from normalized config (simple openapi block). Used as gate. */
126
+ async function detectOpenApiDriftFromNormalized(config, evidenceDir) {
127
+ const openapi = config.openapi;
128
+ const exportLogPath = node_path_1.default.join(evidenceDir, "openapi-export.log");
129
+ const exportResult = await (0, exec_1.execCommand)(openapi.export);
130
+ node_fs_1.default.writeFileSync(exportLogPath, [
131
+ `$ ${openapi.export}`,
132
+ `exitCode: ${exportResult.exitCode}`,
133
+ "\n--- stdout ---",
134
+ exportResult.stdout,
135
+ "\n--- stderr ---",
136
+ exportResult.stderr,
137
+ ].join("\n"), "utf8");
138
+ if (exportResult.exitCode !== 0) {
139
+ return {
140
+ impactedDocs: [openapi.published],
141
+ evidenceFiles: [exportLogPath],
142
+ summary: "OpenAPI export command failed",
143
+ signal: {
144
+ kind: "weak_evidence",
145
+ tier: 2,
146
+ confidence: 0.35,
147
+ evidence: [exportLogPath],
148
+ },
149
+ };
150
+ }
151
+ if (!node_fs_1.default.existsSync(openapi.generated) || !node_fs_1.default.existsSync(openapi.published)) {
152
+ return {
153
+ impactedDocs: [openapi.generated, openapi.published],
154
+ evidenceFiles: [exportLogPath],
155
+ summary: "OpenAPI file(s) missing",
156
+ signal: {
157
+ kind: "weak_evidence",
158
+ tier: 2,
159
+ confidence: 0.35,
160
+ evidence: [exportLogPath],
161
+ },
162
+ };
163
+ }
164
+ const generatedRaw = node_fs_1.default.readFileSync(openapi.generated, "utf8");
165
+ const publishedRaw = node_fs_1.default.readFileSync(openapi.published, "utf8");
166
+ const generatedJson = JSON.parse(generatedRaw);
167
+ const publishedJson = JSON.parse(publishedRaw);
168
+ const normalizedGenerated = (0, json_1.stableStringify)(generatedJson);
169
+ const normalizedPublished = (0, json_1.stableStringify)(publishedJson);
170
+ if (normalizedGenerated === normalizedPublished) {
171
+ return {
172
+ impactedDocs: [openapi.published],
173
+ evidenceFiles: [exportLogPath],
174
+ summary: "No OpenAPI drift detected",
175
+ };
176
+ }
177
+ const summary = summarizeSpecDelta(publishedJson, generatedJson);
178
+ const diffPath = node_path_1.default.join(evidenceDir, "openapi.diff.txt");
179
+ node_fs_1.default.writeFileSync(diffPath, [
180
+ "# OpenAPI Drift Summary",
181
+ summary,
182
+ "",
183
+ "# Published (normalized)",
184
+ normalizedPublished,
185
+ "",
186
+ "# Generated (normalized)",
187
+ normalizedGenerated,
188
+ ].join("\n"), "utf8");
189
+ const impactedDocs = [
190
+ ...new Set([
191
+ openapi.published,
192
+ ...config.docAreas.flatMap((a) => a.patch.targets ?? []).filter(Boolean),
193
+ ]),
194
+ ].filter(Boolean);
195
+ return {
196
+ impactedDocs,
197
+ evidenceFiles: [exportLogPath, diffPath],
198
+ summary,
199
+ signal: {
200
+ kind: "openapi_diff",
201
+ tier: 1,
202
+ confidence: 0.95,
203
+ evidence: [diffPath],
204
+ },
122
205
  };
123
206
  }
@@ -2,11 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildAutogenPrompt = buildAutogenPrompt;
4
4
  exports.buildConceptualPrompt = buildConceptualPrompt;
5
+ exports.buildWholeDocsitePrompt = buildWholeDocsitePrompt;
5
6
  function attachmentBlock(attachmentUrls) {
6
7
  return attachmentUrls.map((url, index) => `- ATTACHMENT ${index + 1}: ${url}`).join("\n");
7
8
  }
8
9
  function buildAutogenPrompt(input) {
9
- return [
10
+ const base = [
10
11
  "You are Devin. Task: update API reference docs to match actual code/spec changes.",
11
12
  "",
12
13
  "EVIDENCE (attachments):",
@@ -30,12 +31,16 @@ function buildAutogenPrompt(input) {
30
31
  " e) get blocked (status=BLOCKED + questions),",
31
32
  " f) complete (status=DONE).",
32
33
  "",
33
- `Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence.`
34
+ `Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence.`,
34
35
  ].join("\n");
36
+ if (input.customAppend) {
37
+ return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
38
+ }
39
+ return base;
35
40
  }
36
41
  function buildConceptualPrompt(input) {
37
- return [
38
- "You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes.",
42
+ const base = [
43
+ "You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes. i..e GUIDES",
39
44
  "",
40
45
  "EVIDENCE (attachments):",
41
46
  attachmentBlock(input.attachmentUrls),
@@ -50,6 +55,52 @@ function buildConceptualPrompt(input) {
50
55
  "- Update at planning, editing, verifying, open-pr, blocked, done milestones.",
51
56
  "- If blocked, fill blocked.questions with concrete, reviewer-actionable questions.",
52
57
  "",
53
- "Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text."
58
+ "Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text.",
54
59
  ].join("\n");
60
+ if (input.customAppend) {
61
+ return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
62
+ }
63
+ return base;
64
+ }
65
+ /** Whole-docsite prompt for single-session runs */
66
+ function buildWholeDocsitePrompt(input) {
67
+ const excludeNote = input.config.exclude?.length > 0
68
+ ? `\n6) NEVER modify files matching these patterns: ${input.config.exclude.join(", ")}`
69
+ : "";
70
+ const requireReviewNote = input.config.requireHumanReview?.length > 0
71
+ ? `\n7) If you touch files under: ${input.config.requireHumanReview.join(", ")} — note it in the PR description (a follow-up issue will flag for human review).`
72
+ : "";
73
+ const allowNewFiles = input.config.policy.allowNewFiles ?? false;
74
+ const newFilesRule = allowNewFiles
75
+ ? "8) You MAY add new articles, create new folders, and change information architecture when warranted."
76
+ : "8) You may ONLY edit existing files. Do NOT create new files, new articles, or new folders. Do NOT change information architecture.";
77
+ const base = [
78
+ "You are Devin. Task: update the entire docsite to match the API and code changes.",
79
+ "",
80
+ "EVIDENCE (attachments):",
81
+ input.attachmentUrls.map((url, i) => `- ATTACHMENT ${i + 1}: ${url}`).join("\n"),
82
+ "",
83
+ "Rules (hard):",
84
+ `1) Only modify files under: ${input.config.policy.allowlist.join(", ")}`,
85
+ "2) Make the smallest change that makes docs correct.",
86
+ "3) Update API reference (OpenAPI) and any impacted guides in one PR.",
87
+ "4) Run verification commands and record results:",
88
+ ...input.config.policy.verification.commands.map((c) => ` - ${c}`),
89
+ "5) Open exactly ONE pull request with a clear title and reviewer-friendly description.",
90
+ `6) Docsite scope: ${input.config.docsite.join(", ")}` +
91
+ excludeNote +
92
+ requireReviewNote +
93
+ `\n${newFilesRule}`,
94
+ "",
95
+ "Structured Output:",
96
+ "- Maintain structured output in the provided JSON schema.",
97
+ "- Update it at: planning, editing, verifying, open-pr, blocked, done.",
98
+ "- If blocked, fill blocked.questions with concrete questions.",
99
+ "",
100
+ "Goal: Produce ONE PR that updates the whole docsite (API reference + guides) using only the evidence.",
101
+ ].join("\n");
102
+ if (input.config.devin.customInstructionContent) {
103
+ return base + "\n\n---\n\nCustom instructions:\n\n" + input.config.devin.customInstructionContent;
104
+ }
105
+ return base;
55
106
  }
@@ -13,7 +13,7 @@ exports.PatchPlanSchema = {
13
13
  "evidence",
14
14
  "filesToEdit",
15
15
  "verification",
16
- "nextAction"
16
+ "nextAction",
17
17
  ],
18
18
  properties: {
19
19
  status: { enum: ["PLANNING", "EDITING", "VERIFYING", "OPENED_PR", "BLOCKED", "DONE"] },
@@ -27,8 +27,8 @@ exports.PatchPlanSchema = {
27
27
  required: ["attachments", "diffSummary"],
28
28
  properties: {
29
29
  attachments: { type: "array", items: { type: "string" } },
30
- diffSummary: { type: "string" }
31
- }
30
+ diffSummary: { type: "string" },
31
+ },
32
32
  },
33
33
  filesToEdit: { type: "array", items: { type: "string" } },
34
34
  verification: {
@@ -37,8 +37,8 @@ exports.PatchPlanSchema = {
37
37
  required: ["commands"],
38
38
  properties: {
39
39
  commands: { type: "array", items: { type: "string" } },
40
- results: { type: "array", items: { type: "string" } }
41
- }
40
+ results: { type: "array", items: { type: "string" } },
41
+ },
42
42
  },
43
43
  nextAction: { enum: ["OPEN_PR", "OPEN_ISSUE", "NOOP"] },
44
44
  pr: {
@@ -46,18 +46,18 @@ exports.PatchPlanSchema = {
46
46
  additionalProperties: false,
47
47
  properties: {
48
48
  title: { type: "string" },
49
- url: { type: "string" }
50
- }
49
+ url: { type: "string" },
50
+ },
51
51
  },
52
52
  blocked: {
53
53
  type: "object",
54
54
  additionalProperties: false,
55
55
  properties: {
56
56
  reason: { type: "string" },
57
- questions: { type: "array", items: { type: "string" } }
58
- }
59
- }
60
- }
57
+ questions: { type: "array", items: { type: "string" } },
58
+ },
59
+ },
60
+ },
61
61
  };
62
62
  exports.PatchResultSchema = {
63
63
  type: "object",
@@ -74,8 +74,8 @@ exports.PatchResultSchema = {
74
74
  required: ["commands", "results"],
75
75
  properties: {
76
76
  commands: { type: "array", items: { type: "string" } },
77
- results: { type: "array", items: { type: "string" } }
78
- }
77
+ results: { type: "array", items: { type: "string" } },
78
+ },
79
79
  },
80
80
  links: {
81
81
  type: "object",
@@ -84,16 +84,16 @@ exports.PatchResultSchema = {
84
84
  properties: {
85
85
  sessionUrl: { type: "string" },
86
86
  prUrl: { type: "string" },
87
- issueUrl: { type: "string" }
88
- }
87
+ issueUrl: { type: "string" },
88
+ },
89
89
  },
90
90
  blocked: {
91
91
  type: "object",
92
92
  additionalProperties: false,
93
93
  properties: {
94
94
  reason: { type: "string" },
95
- questions: { type: "array", items: { type: "string" } }
96
- }
97
- }
98
- }
95
+ questions: { type: "array", items: { type: "string" } },
96
+ },
97
+ },
98
+ },
99
99
  };
@@ -24,9 +24,9 @@ async function devinUploadAttachment(apiKey, filePath) {
24
24
  const response = await fetch("https://api.devin.ai/v1/attachments", {
25
25
  method: "POST",
26
26
  headers: {
27
- Authorization: `Bearer ${apiKey}`
27
+ Authorization: `Bearer ${apiKey}`,
28
28
  },
29
- body: form
29
+ body: form,
30
30
  });
31
31
  const text = await response.text();
32
32
  ensureOk(response, text, "Upload attachment");
@@ -49,9 +49,9 @@ async function devinCreateSession(apiKey, body) {
49
49
  method: "POST",
50
50
  headers: {
51
51
  Authorization: `Bearer ${apiKey}`,
52
- "Content-Type": "application/json"
52
+ "Content-Type": "application/json",
53
53
  },
54
- body: JSON.stringify(body)
54
+ body: JSON.stringify(body),
55
55
  });
56
56
  const text = await response.text();
57
57
  ensureOk(response, text, "Create session");
@@ -60,8 +60,8 @@ async function devinCreateSession(apiKey, body) {
60
60
  async function devinGetSession(apiKey, sessionId) {
61
61
  const response = await fetch(`https://api.devin.ai/v1/sessions/${sessionId}`, {
62
62
  headers: {
63
- Authorization: `Bearer ${apiKey}`
64
- }
63
+ Authorization: `Bearer ${apiKey}`,
64
+ },
65
65
  });
66
66
  const text = await response.text();
67
67
  ensureOk(response, text, "Get session");
@@ -77,8 +77,8 @@ async function devinListSessions(apiKey, params = {}) {
77
77
  }
78
78
  const response = await fetch(url, {
79
79
  headers: {
80
- Authorization: `Bearer ${apiKey}`
81
- }
80
+ Authorization: `Bearer ${apiKey}`,
81
+ },
82
82
  });
83
83
  const text = await response.text();
84
84
  ensureOk(response, text, "List sessions");
@@ -52,7 +52,7 @@ async function buildEvidenceBundle(input) {
52
52
  baseSha: input.runInfo.baseSha,
53
53
  headSha: input.runInfo.headSha,
54
54
  trigger: input.runInfo.trigger,
55
- timestamp: input.runInfo.timestamp
55
+ timestamp: input.runInfo.timestamp,
56
56
  },
57
57
  docArea: input.item.docArea,
58
58
  mode: input.item.mode,
@@ -60,7 +60,7 @@ async function buildEvidenceBundle(input) {
60
60
  signals: input.item.signals,
61
61
  impactedDocs: input.item.impactedDocs,
62
62
  copiedEvidence,
63
- copiedDocs
63
+ copiedDocs,
64
64
  });
65
65
  const archivePath = `${bundleDir}.tar.gz`;
66
66
  const parent = node_path_1.default.dirname(bundleDir);
@@ -73,7 +73,7 @@ async function buildEvidenceBundle(input) {
73
73
  bundleDir,
74
74
  archivePath,
75
75
  manifestPath,
76
- attachmentPaths: [archivePath, manifestPath]
76
+ attachmentPaths: [archivePath, manifestPath],
77
77
  };
78
78
  }
79
79
  function writeMetrics(metrics) {
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRepo = parseRepo;
3
4
  exports.postCommitComment = postCommitComment;
4
5
  exports.createIssue = createIssue;
5
6
  exports.renderRunComment = renderRunComment;
6
7
  exports.renderBlockedIssueBody = renderBlockedIssueBody;
8
+ exports.renderRequireHumanReviewIssueBody = renderRequireHumanReviewIssueBody;
9
+ exports.renderSlaIssueBody = renderSlaIssueBody;
10
+ exports.isPrOpen = isPrOpen;
11
+ exports.listOpenPrsWithLabel = listOpenPrsWithLabel;
7
12
  const rest_1 = require("@octokit/rest");
8
13
  function parseRepo(full) {
9
14
  const [owner, repo] = full.split("/");
@@ -19,7 +24,7 @@ async function postCommitComment(input) {
19
24
  owner,
20
25
  repo,
21
26
  commit_sha: input.commitSha,
22
- body: input.body
27
+ body: input.body,
23
28
  });
24
29
  return response.data.html_url;
25
30
  }
@@ -31,7 +36,7 @@ async function createIssue(input) {
31
36
  repo,
32
37
  title: input.issue.title,
33
38
  body: input.issue.body,
34
- labels: input.issue.labels
39
+ labels: input.issue.labels,
35
40
  });
36
41
  return response.data.html_url;
37
42
  }
@@ -84,3 +89,73 @@ function renderBlockedIssueBody(input) {
84
89
  }
85
90
  return lines.join("\n");
86
91
  }
92
+ function renderRequireHumanReviewIssueBody(input) {
93
+ const lines = [];
94
+ lines.push("## Why this issue");
95
+ lines.push("");
96
+ lines.push("This doc-drift PR touches paths that require human review (guides, prose, or other non-technical docs).");
97
+ lines.push("");
98
+ lines.push("## What to do");
99
+ lines.push("");
100
+ lines.push(`1. Review the PR: ${input.prUrl}`);
101
+ lines.push("2. Confirm the changes are correct or request modifications.");
102
+ lines.push("3. Merge or close the PR.");
103
+ lines.push("");
104
+ if (input.touchedPaths.length > 0) {
105
+ lines.push("## Touched paths (require review)");
106
+ lines.push("");
107
+ for (const p of input.touchedPaths.slice(0, 20)) {
108
+ lines.push(`- \`${p}\``);
109
+ }
110
+ if (input.touchedPaths.length > 20) {
111
+ lines.push(`- ... and ${input.touchedPaths.length - 20} more`);
112
+ }
113
+ }
114
+ return lines.join("\n");
115
+ }
116
+ function renderSlaIssueBody(input) {
117
+ const lines = [];
118
+ lines.push("## Why this issue");
119
+ lines.push("");
120
+ lines.push(`Doc-drift PR(s) have been open for ${input.slaDays}+ days. Docs may be out of sync.`);
121
+ lines.push("");
122
+ lines.push("## What to do");
123
+ lines.push("");
124
+ lines.push("Please review and merge or close the following PR(s):");
125
+ lines.push("");
126
+ for (const url of input.prUrls) {
127
+ lines.push(`- ${url}`);
128
+ }
129
+ lines.push("");
130
+ lines.push("If the PR is no longer needed, close it to resolve this reminder.");
131
+ return lines.join("\n");
132
+ }
133
+ /** Check if a PR is still open. URL format: https://github.com/owner/repo/pull/123 */
134
+ async function isPrOpen(token, prUrl) {
135
+ const match = prUrl.match(/github\.com[/]([^/]+)[/]([^/]+)[/]pull[/](\d+)/);
136
+ if (!match)
137
+ return { open: false };
138
+ const [, owner, repo, numStr] = match;
139
+ const number = parseInt(numStr ?? "0", 10);
140
+ if (!owner || !repo || !Number.isFinite(number))
141
+ return { open: false };
142
+ const octokit = new rest_1.Octokit({ auth: token });
143
+ const { data } = await octokit.pulls.get({ owner, repo, pull_number: number });
144
+ return { open: data.state === "open", number: data.number };
145
+ }
146
+ /** List open PRs with a given label */
147
+ async function listOpenPrsWithLabel(token, repository, label) {
148
+ const octokit = new rest_1.Octokit({ auth: token });
149
+ const { owner, repo } = parseRepo(repository);
150
+ const { data } = await octokit.pulls.list({
151
+ owner,
152
+ repo,
153
+ state: "open",
154
+ labels: label,
155
+ });
156
+ return data.map((pr) => ({
157
+ url: pr.html_url ?? "",
158
+ number: pr.number,
159
+ created_at: pr.created_at ?? "",
160
+ }));
161
+ }