@devinnn/docdrift 0.1.1 → 0.1.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/README.md +15 -13
- package/dist/src/cli.js +15 -9
- package/dist/src/config/load.js +7 -0
- package/dist/src/config/normalize.js +155 -0
- package/dist/src/config/schema.js +112 -24
- package/dist/src/config/validate.js +9 -6
- package/dist/src/detect/index.js +110 -46
- package/dist/src/detect/openapi.js +83 -0
- package/dist/src/devin/prompts.js +87 -0
- package/dist/src/github/client.js +88 -0
- package/dist/src/index.js +298 -151
- package/dist/src/policy/engine.js +2 -1
- package/dist/src/spec-providers/fern.js +123 -0
- package/dist/src/spec-providers/graphql.js +168 -0
- package/dist/src/spec-providers/openapi.js +181 -0
- package/dist/src/spec-providers/postman.js +193 -0
- package/dist/src/spec-providers/registry.js +26 -0
- package/dist/src/spec-providers/swagger2.js +229 -0
- package/dist/src/spec-providers/types.js +2 -0
- package/dist/src/utils/fetch.js +87 -0
- package/dist/src/utils/git.js +20 -0
- package/dist/src/utils/glob.js +13 -0
- package/docdrift.schema.json +438 -0
- package/package.json +9 -4
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -7,10 +40,12 @@ exports.STATE_PATH = void 0;
|
|
|
7
40
|
exports.runDetect = runDetect;
|
|
8
41
|
exports.runDocDrift = runDocDrift;
|
|
9
42
|
exports.runValidate = runValidate;
|
|
43
|
+
exports.runSlaCheck = runSlaCheck;
|
|
10
44
|
exports.runStatus = runStatus;
|
|
11
45
|
exports.resolveTrigger = resolveTrigger;
|
|
12
46
|
exports.parseDurationHours = parseDurationHours;
|
|
13
47
|
exports.requireSha = requireSha;
|
|
48
|
+
exports.resolveBaseHead = resolveBaseHead;
|
|
14
49
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
50
|
const load_1 = require("./config/load");
|
|
16
51
|
const validate_1 = require("./config/validate");
|
|
@@ -21,6 +56,7 @@ const engine_1 = require("./policy/engine");
|
|
|
21
56
|
const state_1 = require("./policy/state");
|
|
22
57
|
const log_1 = require("./utils/log");
|
|
23
58
|
const prompts_1 = require("./devin/prompts");
|
|
59
|
+
const glob_1 = require("./utils/glob");
|
|
24
60
|
const schemas_1 = require("./devin/schemas");
|
|
25
61
|
const v1_1 = require("./devin/v1");
|
|
26
62
|
function parseStructured(session) {
|
|
@@ -48,29 +84,20 @@ function inferQuestions(structured) {
|
|
|
48
84
|
"What are the exact user-visible semantics after this merge?",
|
|
49
85
|
];
|
|
50
86
|
}
|
|
51
|
-
async function
|
|
87
|
+
async function executeSessionSingle(input) {
|
|
52
88
|
const attachmentUrls = [];
|
|
53
89
|
for (const attachmentPath of input.attachmentPaths) {
|
|
54
90
|
const url = await (0, v1_1.devinUploadAttachment)(input.apiKey, attachmentPath);
|
|
55
91
|
attachmentUrls.push(url);
|
|
56
92
|
}
|
|
57
|
-
const prompt =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
})
|
|
66
|
-
: (0, prompts_1.buildConceptualPrompt)({
|
|
67
|
-
item: input.item,
|
|
68
|
-
attachmentUrls,
|
|
69
|
-
verificationCommands: input.config.policy.verification.commands,
|
|
70
|
-
allowlist: input.config.policy.allowlist,
|
|
71
|
-
confidenceThreshold: input.config.policy.confidence.autopatchThreshold,
|
|
72
|
-
customAppend: input.config.devin.customInstructionContent ?? undefined,
|
|
73
|
-
});
|
|
93
|
+
const prompt = (0, prompts_1.buildWholeDocsitePrompt)({
|
|
94
|
+
aggregated: input.aggregated,
|
|
95
|
+
config: input.config,
|
|
96
|
+
attachmentUrls,
|
|
97
|
+
runGate: input.runGate,
|
|
98
|
+
trigger: input.trigger,
|
|
99
|
+
prNumber: input.prNumber,
|
|
100
|
+
});
|
|
74
101
|
const session = await (0, v1_1.devinCreateSession)(input.apiKey, {
|
|
75
102
|
prompt,
|
|
76
103
|
unlisted: input.config.devin.unlisted,
|
|
@@ -132,14 +159,16 @@ async function runDetect(options) {
|
|
|
132
159
|
throw new Error(`Config validation failed:\n${runtimeValidation.errors.join("\n")}`);
|
|
133
160
|
}
|
|
134
161
|
const repo = process.env.GITHUB_REPOSITORY ?? "local/docdrift";
|
|
135
|
-
const
|
|
136
|
-
|
|
162
|
+
const normalized = (0, load_1.loadNormalizedConfig)();
|
|
163
|
+
const { report, runGate } = await (0, detect_1.buildDriftReport)({
|
|
164
|
+
config: normalized,
|
|
137
165
|
repo,
|
|
138
166
|
baseSha: options.baseSha,
|
|
139
167
|
headSha: options.headSha,
|
|
140
168
|
trigger: options.trigger ?? "manual",
|
|
169
|
+
prNumber: options.prNumber,
|
|
141
170
|
});
|
|
142
|
-
(0, log_1.logInfo)(`Drift items detected: ${report.items.length}`);
|
|
171
|
+
(0, log_1.logInfo)(`Drift items detected: ${report.items.length} (runGate: ${runGate})`);
|
|
143
172
|
return { hasDrift: report.items.length > 0 };
|
|
144
173
|
}
|
|
145
174
|
async function runDocDrift(options) {
|
|
@@ -148,183 +177,245 @@ async function runDocDrift(options) {
|
|
|
148
177
|
if (runtimeValidation.errors.length) {
|
|
149
178
|
throw new Error(`Config validation failed:\n${runtimeValidation.errors.join("\n")}`);
|
|
150
179
|
}
|
|
180
|
+
const normalized = (0, load_1.loadNormalizedConfig)();
|
|
151
181
|
const repo = process.env.GITHUB_REPOSITORY ?? "local/docdrift";
|
|
152
182
|
const commitSha = process.env.GITHUB_SHA ?? options.headSha;
|
|
153
183
|
const githubToken = process.env.GITHUB_TOKEN;
|
|
154
184
|
const devinApiKey = process.env.DEVIN_API_KEY;
|
|
155
|
-
const { report, runInfo, evidenceRoot } = await (0, detect_1.buildDriftReport)({
|
|
156
|
-
config,
|
|
185
|
+
const { report, aggregated, runInfo, evidenceRoot, runGate } = await (0, detect_1.buildDriftReport)({
|
|
186
|
+
config: normalized,
|
|
157
187
|
repo,
|
|
158
188
|
baseSha: options.baseSha,
|
|
159
189
|
headSha: options.headSha,
|
|
160
190
|
trigger: options.trigger ?? "manual",
|
|
191
|
+
prNumber: options.prNumber,
|
|
161
192
|
});
|
|
162
|
-
|
|
193
|
+
// Gate: no run (spec drift, conceptual-only, or infer) — exit early, no session
|
|
194
|
+
if (runGate === "none" || report.items.length === 0) {
|
|
195
|
+
(0, log_1.logInfo)("No drift; skipping session");
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const item = report.items[0];
|
|
199
|
+
const docAreaConfig = {
|
|
200
|
+
name: "docsite",
|
|
201
|
+
mode: "autogen",
|
|
202
|
+
owners: { reviewers: [] },
|
|
203
|
+
detect: { openapi: { exportCmd: normalized.openapi.export, generatedPath: normalized.openapi.generated, publishedPath: normalized.openapi.published }, paths: [] },
|
|
204
|
+
patch: { targets: [], requireHumanConfirmation: false },
|
|
205
|
+
};
|
|
163
206
|
let state = (0, state_1.loadState)();
|
|
164
207
|
const startedAt = Date.now();
|
|
165
208
|
const results = [];
|
|
166
209
|
const metrics = {
|
|
167
|
-
driftItemsDetected:
|
|
210
|
+
driftItemsDetected: 1,
|
|
168
211
|
prsOpened: 0,
|
|
169
212
|
issuesOpened: 0,
|
|
170
213
|
blockedCount: 0,
|
|
171
214
|
timeToSessionTerminalMs: [],
|
|
172
|
-
docAreaCounts: {},
|
|
215
|
+
docAreaCounts: { docsite: 1 },
|
|
173
216
|
noiseRateProxy: 0,
|
|
174
217
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
218
|
+
const decision = (0, engine_1.decidePolicy)({
|
|
219
|
+
item,
|
|
220
|
+
docAreaConfig,
|
|
221
|
+
config,
|
|
222
|
+
state,
|
|
223
|
+
repo,
|
|
224
|
+
baseSha: options.baseSha,
|
|
225
|
+
headSha: options.headSha,
|
|
226
|
+
});
|
|
227
|
+
if (decision.action === "NOOP") {
|
|
228
|
+
results.push({
|
|
229
|
+
docArea: item.docArea,
|
|
230
|
+
decision,
|
|
231
|
+
outcome: "NO_CHANGE",
|
|
232
|
+
summary: decision.reason,
|
|
233
|
+
});
|
|
234
|
+
(0, bundle_1.writeMetrics)(metrics);
|
|
235
|
+
return results;
|
|
236
|
+
}
|
|
237
|
+
if (decision.action === "UPDATE_EXISTING_PR") {
|
|
238
|
+
const existingPr = state.areaLatestPr["docsite"];
|
|
239
|
+
results.push({
|
|
240
|
+
docArea: item.docArea,
|
|
241
|
+
decision,
|
|
242
|
+
outcome: existingPr ? "NO_CHANGE" : "BLOCKED",
|
|
243
|
+
summary: existingPr ? `Bundled into existing PR: ${existingPr}` : "PR cap reached",
|
|
244
|
+
prUrl: existingPr,
|
|
245
|
+
});
|
|
246
|
+
state = (0, engine_1.applyDecisionToState)({
|
|
185
247
|
state,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
248
|
+
decision,
|
|
249
|
+
docArea: "docsite",
|
|
250
|
+
outcome: existingPr ? "NO_CHANGE" : "BLOCKED",
|
|
251
|
+
link: existingPr,
|
|
189
252
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
verification: config.policy.verification.commands.map((command) => ({
|
|
253
|
+
(0, state_1.saveState)(state);
|
|
254
|
+
(0, bundle_1.writeMetrics)(metrics);
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
const bundle = await (0, bundle_1.buildEvidenceBundle)({ runInfo, item, evidenceRoot });
|
|
258
|
+
const attachmentPaths = [...new Set([bundle.archivePath, ...bundle.attachmentPaths])];
|
|
259
|
+
let sessionOutcome = {
|
|
260
|
+
outcome: "NO_CHANGE",
|
|
261
|
+
summary: "Skipped Devin session",
|
|
262
|
+
verification: normalized.policy.verification.commands.map((command) => ({
|
|
263
|
+
command,
|
|
264
|
+
result: "not run",
|
|
265
|
+
})),
|
|
266
|
+
};
|
|
267
|
+
if (devinApiKey) {
|
|
268
|
+
const sessionStart = Date.now();
|
|
269
|
+
sessionOutcome = await executeSessionSingle({
|
|
270
|
+
apiKey: devinApiKey,
|
|
271
|
+
repository: repo,
|
|
272
|
+
item,
|
|
273
|
+
aggregated: aggregated,
|
|
274
|
+
attachmentPaths,
|
|
275
|
+
config: normalized,
|
|
276
|
+
runGate,
|
|
277
|
+
trigger: runInfo.trigger,
|
|
278
|
+
prNumber: runInfo.prNumber,
|
|
279
|
+
});
|
|
280
|
+
metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
(0, log_1.logWarn)("DEVIN_API_KEY not set; running fallback behavior", { docArea: item.docArea });
|
|
284
|
+
sessionOutcome = {
|
|
285
|
+
outcome: "BLOCKED",
|
|
286
|
+
summary: "DEVIN_API_KEY missing; cannot start Devin session",
|
|
287
|
+
questions: ["Set DEVIN_API_KEY in environment or GitHub Actions secrets"],
|
|
288
|
+
verification: normalized.policy.verification.commands.map((command) => ({
|
|
227
289
|
command,
|
|
228
290
|
result: "not run",
|
|
229
291
|
})),
|
|
230
292
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
293
|
+
}
|
|
294
|
+
let issueUrl;
|
|
295
|
+
if (sessionOutcome.outcome === "PR_OPENED" && sessionOutcome.prUrl) {
|
|
296
|
+
metrics.prsOpened += 1;
|
|
297
|
+
state.lastDocDriftPrUrl = sessionOutcome.prUrl;
|
|
298
|
+
state.lastDocDriftPrOpenedAt = new Date().toISOString();
|
|
299
|
+
if (githubToken && runInfo.trigger === "pull_request" && runInfo.prNumber) {
|
|
300
|
+
await (0, client_1.postPrComment)({
|
|
301
|
+
token: githubToken,
|
|
235
302
|
repository: repo,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
config,
|
|
303
|
+
prNumber: runInfo.prNumber,
|
|
304
|
+
body: `## Doc drift detected\n\nDraft doc PR: ${sessionOutcome.prUrl}\n\nMerge your API changes first, then review and merge this doc PR.`,
|
|
239
305
|
});
|
|
240
|
-
metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
(0, log_1.logWarn)("DEVIN_API_KEY not set; running fallback behavior", { docArea: item.docArea });
|
|
244
|
-
sessionOutcome = {
|
|
245
|
-
outcome: "BLOCKED",
|
|
246
|
-
summary: "DEVIN_API_KEY missing; cannot start Devin session",
|
|
247
|
-
questions: ["Set DEVIN_API_KEY in environment or GitHub Actions secrets"],
|
|
248
|
-
verification: config.policy.verification.commands.map((command) => ({
|
|
249
|
-
command,
|
|
250
|
-
result: "not run",
|
|
251
|
-
})),
|
|
252
|
-
};
|
|
253
306
|
}
|
|
254
|
-
|
|
255
|
-
if (githubToken &&
|
|
256
|
-
(decision.action === "OPEN_ISSUE" ||
|
|
257
|
-
sessionOutcome.outcome === "BLOCKED" ||
|
|
258
|
-
sessionOutcome.outcome === "NO_CHANGE")) {
|
|
307
|
+
const touchedRequireReview = (item.impactedDocs ?? []).filter((p) => normalized.requireHumanReview.some((glob) => (0, glob_1.matchesGlob)(glob, p)));
|
|
308
|
+
if (githubToken && touchedRequireReview.length > 0) {
|
|
259
309
|
issueUrl = await (0, client_1.createIssue)({
|
|
260
310
|
token: githubToken,
|
|
261
311
|
repository: repo,
|
|
262
312
|
issue: {
|
|
263
|
-
title:
|
|
264
|
-
body: (0, client_1.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
questions: sessionOutcome.questions ?? [
|
|
268
|
-
"Please confirm intended behavior and doc wording.",
|
|
269
|
-
],
|
|
270
|
-
sessionUrl: sessionOutcome.sessionUrl,
|
|
313
|
+
title: "[docdrift] Docs out of sync — review doc drift PR",
|
|
314
|
+
body: (0, client_1.renderRequireHumanReviewIssueBody)({
|
|
315
|
+
prUrl: sessionOutcome.prUrl,
|
|
316
|
+
touchedPaths: touchedRequireReview,
|
|
271
317
|
}),
|
|
272
318
|
labels: ["docdrift"],
|
|
273
319
|
},
|
|
274
320
|
});
|
|
275
321
|
metrics.issuesOpened += 1;
|
|
276
|
-
sessionOutcome.outcome = "ISSUE_OPENED";
|
|
277
|
-
}
|
|
278
|
-
if (sessionOutcome.outcome === "PR_OPENED") {
|
|
279
|
-
metrics.prsOpened += 1;
|
|
280
322
|
}
|
|
281
|
-
|
|
282
|
-
|
|
323
|
+
}
|
|
324
|
+
else if (githubToken &&
|
|
325
|
+
(decision.action === "OPEN_ISSUE" ||
|
|
326
|
+
sessionOutcome.outcome === "BLOCKED" ||
|
|
327
|
+
sessionOutcome.outcome === "NO_CHANGE")) {
|
|
328
|
+
issueUrl = await (0, client_1.createIssue)({
|
|
329
|
+
token: githubToken,
|
|
330
|
+
repository: repo,
|
|
331
|
+
issue: {
|
|
332
|
+
title: "[docdrift] docsite: docs drift requires input",
|
|
333
|
+
body: (0, client_1.renderBlockedIssueBody)({
|
|
334
|
+
docArea: item.docArea,
|
|
335
|
+
evidenceSummary: item.summary,
|
|
336
|
+
questions: sessionOutcome.questions ?? [
|
|
337
|
+
"Please confirm intended behavior and doc wording.",
|
|
338
|
+
],
|
|
339
|
+
sessionUrl: sessionOutcome.sessionUrl,
|
|
340
|
+
}),
|
|
341
|
+
labels: ["docdrift"],
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
metrics.issuesOpened += 1;
|
|
345
|
+
if (sessionOutcome.outcome !== "PR_OPENED") {
|
|
346
|
+
sessionOutcome.outcome = "ISSUE_OPENED";
|
|
283
347
|
}
|
|
284
|
-
|
|
348
|
+
}
|
|
349
|
+
if (sessionOutcome.outcome === "BLOCKED") {
|
|
350
|
+
metrics.blockedCount += 1;
|
|
351
|
+
}
|
|
352
|
+
const result = {
|
|
353
|
+
docArea: item.docArea,
|
|
354
|
+
decision,
|
|
355
|
+
outcome: sessionOutcome.outcome,
|
|
356
|
+
summary: sessionOutcome.summary,
|
|
357
|
+
sessionUrl: sessionOutcome.sessionUrl,
|
|
358
|
+
prUrl: sessionOutcome.prUrl,
|
|
359
|
+
issueUrl,
|
|
360
|
+
};
|
|
361
|
+
results.push(result);
|
|
362
|
+
state = (0, engine_1.applyDecisionToState)({
|
|
363
|
+
state,
|
|
364
|
+
decision,
|
|
365
|
+
docArea: "docsite",
|
|
366
|
+
outcome: sessionOutcome.outcome,
|
|
367
|
+
link: sessionOutcome.prUrl ?? issueUrl,
|
|
368
|
+
});
|
|
369
|
+
if (sessionOutcome.outcome === "PR_OPENED" && sessionOutcome.prUrl) {
|
|
370
|
+
state.lastDocDriftPrUrl = sessionOutcome.prUrl;
|
|
371
|
+
state.lastDocDriftPrOpenedAt = new Date().toISOString();
|
|
372
|
+
}
|
|
373
|
+
(0, state_1.saveState)(state);
|
|
374
|
+
if (githubToken) {
|
|
375
|
+
const body = (0, client_1.renderRunComment)({
|
|
285
376
|
docArea: item.docArea,
|
|
286
|
-
decision,
|
|
287
|
-
outcome: sessionOutcome.outcome,
|
|
288
377
|
summary: sessionOutcome.summary,
|
|
378
|
+
decision: decision.action,
|
|
379
|
+
outcome: sessionOutcome.outcome,
|
|
289
380
|
sessionUrl: sessionOutcome.sessionUrl,
|
|
290
381
|
prUrl: sessionOutcome.prUrl,
|
|
291
382
|
issueUrl,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
383
|
+
validation: sessionOutcome.verification,
|
|
384
|
+
});
|
|
385
|
+
await (0, client_1.postCommitComment)({
|
|
386
|
+
token: githubToken,
|
|
387
|
+
repository: repo,
|
|
388
|
+
commitSha,
|
|
389
|
+
body,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const slaDays = normalized.policy.slaDays ?? 0;
|
|
393
|
+
if (githubToken && slaDays > 0 && state.lastDocDriftPrUrl && state.lastDocDriftPrOpenedAt) {
|
|
394
|
+
const openedAt = Date.parse(state.lastDocDriftPrOpenedAt);
|
|
395
|
+
const daysOld = (Date.now() - openedAt) / (24 * 60 * 60 * 1000);
|
|
396
|
+
const lastSla = state.lastSlaIssueOpenedAt ? Date.parse(state.lastSlaIssueOpenedAt) : 0;
|
|
397
|
+
const slaCooldown = 6 * 24 * 60 * 60 * 1000;
|
|
398
|
+
if (daysOld >= slaDays && Date.now() - lastSla > slaCooldown) {
|
|
399
|
+
const slaIssueUrl = await (0, client_1.createIssue)({
|
|
306
400
|
token: githubToken,
|
|
307
401
|
repository: repo,
|
|
308
|
-
|
|
309
|
-
|
|
402
|
+
issue: {
|
|
403
|
+
title: "[docdrift] Docs out of sync — merge doc drift PR(s)",
|
|
404
|
+
body: (0, client_1.renderSlaIssueBody)({
|
|
405
|
+
prUrls: [state.lastDocDriftPrUrl],
|
|
406
|
+
slaDays,
|
|
407
|
+
}),
|
|
408
|
+
labels: ["docdrift"],
|
|
409
|
+
},
|
|
310
410
|
});
|
|
411
|
+
state.lastSlaIssueOpenedAt = new Date().toISOString();
|
|
412
|
+
(0, state_1.saveState)(state);
|
|
311
413
|
}
|
|
312
|
-
state = (0, engine_1.applyDecisionToState)({
|
|
313
|
-
state,
|
|
314
|
-
decision,
|
|
315
|
-
docArea: item.docArea,
|
|
316
|
-
outcome: sessionOutcome.outcome,
|
|
317
|
-
link: sessionOutcome.prUrl ?? issueUrl,
|
|
318
|
-
});
|
|
319
414
|
}
|
|
320
|
-
|
|
321
|
-
metrics.noiseRateProxy =
|
|
322
|
-
metrics.driftItemsDetected === 0
|
|
323
|
-
? 0
|
|
324
|
-
: Number((metrics.prsOpened / metrics.driftItemsDetected).toFixed(4));
|
|
415
|
+
metrics.noiseRateProxy = metrics.prsOpened;
|
|
325
416
|
(0, bundle_1.writeMetrics)(metrics);
|
|
326
417
|
(0, log_1.logInfo)("Run complete", {
|
|
327
|
-
items:
|
|
418
|
+
items: 1,
|
|
328
419
|
elapsedMs: Date.now() - startedAt,
|
|
329
420
|
});
|
|
330
421
|
return results;
|
|
@@ -338,6 +429,54 @@ async function runValidate() {
|
|
|
338
429
|
runtimeValidation.warnings.forEach((warning) => (0, log_1.logWarn)(warning));
|
|
339
430
|
(0, log_1.logInfo)("Config is valid");
|
|
340
431
|
}
|
|
432
|
+
async function runSlaCheck() {
|
|
433
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
434
|
+
if (!githubToken) {
|
|
435
|
+
throw new Error("GITHUB_TOKEN is required for sla-check command");
|
|
436
|
+
}
|
|
437
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
438
|
+
if (!repo) {
|
|
439
|
+
throw new Error("GITHUB_REPOSITORY is required for sla-check command");
|
|
440
|
+
}
|
|
441
|
+
const normalized = (0, load_1.loadNormalizedConfig)();
|
|
442
|
+
const slaDays = normalized.policy.slaDays ?? 0;
|
|
443
|
+
const slaLabel = normalized.policy.slaLabel ?? "docdrift";
|
|
444
|
+
if (slaDays <= 0) {
|
|
445
|
+
(0, log_1.logInfo)("SLA check disabled (slaDays <= 0)");
|
|
446
|
+
return { issueOpened: false };
|
|
447
|
+
}
|
|
448
|
+
const cutoff = new Date(Date.now() - slaDays * 24 * 60 * 60 * 1000);
|
|
449
|
+
const openPrs = await (0, client_1.listOpenPrsWithLabel)(githubToken, repo, slaLabel);
|
|
450
|
+
const stalePrs = openPrs.filter((pr) => {
|
|
451
|
+
const created = pr.created_at ? Date.parse(pr.created_at) : Date.now();
|
|
452
|
+
return Number.isFinite(created) && created <= cutoff.getTime();
|
|
453
|
+
});
|
|
454
|
+
if (stalePrs.length === 0) {
|
|
455
|
+
(0, log_1.logInfo)("No doc-drift PRs open longer than slaDays; nothing to do");
|
|
456
|
+
return { issueOpened: false };
|
|
457
|
+
}
|
|
458
|
+
let state = (0, state_1.loadState)();
|
|
459
|
+
const lastSla = state.lastSlaIssueOpenedAt ? Date.parse(state.lastSlaIssueOpenedAt) : 0;
|
|
460
|
+
const slaCooldown = 6 * 24 * 60 * 60 * 1000;
|
|
461
|
+
if (Date.now() - lastSla < slaCooldown) {
|
|
462
|
+
(0, log_1.logInfo)("SLA issue cooldown; skipping");
|
|
463
|
+
return { issueOpened: false };
|
|
464
|
+
}
|
|
465
|
+
const prUrls = stalePrs.map((p) => p.url).filter(Boolean);
|
|
466
|
+
await (0, client_1.createIssue)({
|
|
467
|
+
token: githubToken,
|
|
468
|
+
repository: repo,
|
|
469
|
+
issue: {
|
|
470
|
+
title: "[docdrift] Docs out of sync — merge doc drift PR(s)",
|
|
471
|
+
body: (0, client_1.renderSlaIssueBody)({ prUrls, slaDays }),
|
|
472
|
+
labels: ["docdrift"],
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
state.lastSlaIssueOpenedAt = new Date().toISOString();
|
|
476
|
+
(0, state_1.saveState)(state);
|
|
477
|
+
(0, log_1.logInfo)(`Opened SLA issue for ${prUrls.length} stale PR(s)`);
|
|
478
|
+
return { issueOpened: true };
|
|
479
|
+
}
|
|
341
480
|
async function runStatus(sinceHours = 24) {
|
|
342
481
|
const apiKey = process.env.DEVIN_API_KEY;
|
|
343
482
|
if (!apiKey) {
|
|
@@ -361,12 +500,12 @@ async function runStatus(sinceHours = 24) {
|
|
|
361
500
|
}
|
|
362
501
|
}
|
|
363
502
|
function resolveTrigger(eventName) {
|
|
364
|
-
if (eventName === "push")
|
|
503
|
+
if (eventName === "push")
|
|
365
504
|
return "push";
|
|
366
|
-
|
|
367
|
-
if (eventName === "schedule") {
|
|
505
|
+
if (eventName === "schedule")
|
|
368
506
|
return "schedule";
|
|
369
|
-
|
|
507
|
+
if (eventName === "pull_request")
|
|
508
|
+
return "pull_request";
|
|
370
509
|
return "manual";
|
|
371
510
|
}
|
|
372
511
|
function parseDurationHours(value) {
|
|
@@ -386,4 +525,12 @@ function requireSha(value, label) {
|
|
|
386
525
|
}
|
|
387
526
|
return value;
|
|
388
527
|
}
|
|
528
|
+
async function resolveBaseHead(baseArg, headArg) {
|
|
529
|
+
const headRef = headArg ?? process.env.GITHUB_SHA ?? "HEAD";
|
|
530
|
+
if (baseArg) {
|
|
531
|
+
return { baseSha: baseArg, headSha: headRef };
|
|
532
|
+
}
|
|
533
|
+
const { resolveDefaultBaseHead } = await Promise.resolve().then(() => __importStar(require("./utils/git")));
|
|
534
|
+
return resolveDefaultBaseHead(headRef);
|
|
535
|
+
}
|
|
389
536
|
exports.STATE_PATH = node_path_1.default.resolve(".docdrift", "state.json");
|
|
@@ -21,7 +21,8 @@ function decidePolicy(input) {
|
|
|
21
21
|
const capReached = prCountToday >= config.policy.prCaps.maxPrsPerDay;
|
|
22
22
|
const areaDailyKey = `${today}:${item.docArea}`;
|
|
23
23
|
const exceedsFileCap = item.impactedDocs.length > config.policy.prCaps.maxFilesTouched;
|
|
24
|
-
const
|
|
24
|
+
const exclude = "exclude" in config && Array.isArray(config.exclude) ? config.exclude : [];
|
|
25
|
+
const hasPathOutsideAllowlist = item.impactedDocs.some((filePath) => filePath && !(0, glob_1.isPathAllowedAndNotExcluded)(filePath, config.policy.allowlist, exclude));
|
|
25
26
|
let action = "NOOP";
|
|
26
27
|
let reason = "No action needed";
|
|
27
28
|
if (hasPathOutsideAllowlist) {
|