@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.
@@ -0,0 +1,375 @@
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.STATE_PATH = void 0;
7
+ exports.runDetect = runDetect;
8
+ exports.runDocDrift = runDocDrift;
9
+ exports.runValidate = runValidate;
10
+ exports.runStatus = runStatus;
11
+ exports.resolveTrigger = resolveTrigger;
12
+ exports.parseDurationHours = parseDurationHours;
13
+ exports.requireSha = requireSha;
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const load_1 = require("./config/load");
16
+ const validate_1 = require("./config/validate");
17
+ const detect_1 = require("./detect");
18
+ const bundle_1 = require("./evidence/bundle");
19
+ const client_1 = require("./github/client");
20
+ const engine_1 = require("./policy/engine");
21
+ const state_1 = require("./policy/state");
22
+ const log_1 = require("./utils/log");
23
+ const prompts_1 = require("./devin/prompts");
24
+ const schemas_1 = require("./devin/schemas");
25
+ const v1_1 = require("./devin/v1");
26
+ function parseStructured(session) {
27
+ return session?.structured_output ?? session?.data?.structured_output ?? {};
28
+ }
29
+ function inferPrUrl(session, structured) {
30
+ if (typeof structured?.pr?.url === "string") {
31
+ return structured.pr.url;
32
+ }
33
+ if (typeof session?.pull_request_url === "string") {
34
+ return session.pull_request_url;
35
+ }
36
+ if (typeof session?.pr_url === "string") {
37
+ return session.pr_url;
38
+ }
39
+ return undefined;
40
+ }
41
+ function inferQuestions(structured) {
42
+ const questions = structured?.blocked?.questions;
43
+ if (Array.isArray(questions)) {
44
+ return questions.map(String);
45
+ }
46
+ return [
47
+ "Which conceptual docs should be updated for this behavior change?",
48
+ "What are the exact user-visible semantics after this merge?"
49
+ ];
50
+ }
51
+ async function executeSession(input) {
52
+ const attachmentUrls = [];
53
+ for (const attachmentPath of input.attachmentPaths) {
54
+ const url = await (0, v1_1.devinUploadAttachment)(input.apiKey, attachmentPath);
55
+ attachmentUrls.push(url);
56
+ }
57
+ const prompt = input.item.mode === "autogen"
58
+ ? (0, prompts_1.buildAutogenPrompt)({
59
+ item: input.item,
60
+ attachmentUrls,
61
+ verificationCommands: input.config.policy.verification.commands,
62
+ allowlist: input.config.policy.allowlist,
63
+ confidenceThreshold: input.config.policy.confidence.autopatchThreshold
64
+ })
65
+ : (0, prompts_1.buildConceptualPrompt)({
66
+ item: input.item,
67
+ attachmentUrls,
68
+ verificationCommands: input.config.policy.verification.commands,
69
+ allowlist: input.config.policy.allowlist,
70
+ confidenceThreshold: input.config.policy.confidence.autopatchThreshold
71
+ });
72
+ const session = await (0, v1_1.devinCreateSession)(input.apiKey, {
73
+ prompt,
74
+ unlisted: input.config.devin.unlisted,
75
+ max_acu_limit: input.config.devin.maxAcuLimit,
76
+ tags: [...new Set([...(input.config.devin.tags ?? []), "docdrift", input.item.docArea])],
77
+ attachments: attachmentUrls,
78
+ structured_output: {
79
+ schema: schemas_1.PatchPlanSchema
80
+ },
81
+ metadata: {
82
+ repository: input.repository,
83
+ docArea: input.item.docArea,
84
+ mode: input.item.mode
85
+ }
86
+ });
87
+ const finalSession = await (0, v1_1.pollUntilTerminal)(input.apiKey, session.session_id);
88
+ const structured = parseStructured(finalSession);
89
+ const status = String(finalSession.status_enum ?? finalSession.status ?? "").toLowerCase();
90
+ const prUrl = inferPrUrl(finalSession, structured);
91
+ const verificationCommands = Array.isArray(structured?.verification?.commands)
92
+ ? structured.verification.commands.map(String)
93
+ : input.config.policy.verification.commands;
94
+ const verificationResults = Array.isArray(structured?.verification?.results)
95
+ ? structured.verification.results.map(String)
96
+ : verificationCommands.map(() => "not reported");
97
+ const verification = verificationCommands.map((command, idx) => ({
98
+ command,
99
+ result: verificationResults[idx] ?? "not reported"
100
+ }));
101
+ if (prUrl) {
102
+ return {
103
+ outcome: "PR_OPENED",
104
+ summary: String(structured?.summary ?? "PR opened by Devin"),
105
+ sessionUrl: session.url,
106
+ prUrl,
107
+ verification
108
+ };
109
+ }
110
+ if (status === "blocked" || structured?.status === "BLOCKED") {
111
+ return {
112
+ outcome: "BLOCKED",
113
+ summary: String(structured?.blocked?.reason ?? structured?.summary ?? "Session blocked"),
114
+ sessionUrl: session.url,
115
+ questions: inferQuestions(structured),
116
+ verification
117
+ };
118
+ }
119
+ return {
120
+ outcome: "NO_CHANGE",
121
+ summary: String(structured?.summary ?? "Session completed without PR"),
122
+ sessionUrl: session.url,
123
+ verification
124
+ };
125
+ }
126
+ async function runDetect(options) {
127
+ const config = (0, load_1.loadConfig)();
128
+ const runtimeValidation = await (0, validate_1.validateRuntimeConfig)(config);
129
+ if (runtimeValidation.errors.length) {
130
+ throw new Error(`Config validation failed:\n${runtimeValidation.errors.join("\n")}`);
131
+ }
132
+ const repo = process.env.GITHUB_REPOSITORY ?? "local/docdrift";
133
+ const { report } = await (0, detect_1.buildDriftReport)({
134
+ config,
135
+ repo,
136
+ baseSha: options.baseSha,
137
+ headSha: options.headSha,
138
+ trigger: options.trigger ?? "manual"
139
+ });
140
+ (0, log_1.logInfo)(`Drift items detected: ${report.items.length}`);
141
+ return { hasDrift: report.items.length > 0 };
142
+ }
143
+ async function runDocDrift(options) {
144
+ const config = (0, load_1.loadConfig)();
145
+ const runtimeValidation = await (0, validate_1.validateRuntimeConfig)(config);
146
+ if (runtimeValidation.errors.length) {
147
+ throw new Error(`Config validation failed:\n${runtimeValidation.errors.join("\n")}`);
148
+ }
149
+ const repo = process.env.GITHUB_REPOSITORY ?? "local/docdrift";
150
+ const commitSha = process.env.GITHUB_SHA ?? options.headSha;
151
+ const githubToken = process.env.GITHUB_TOKEN;
152
+ const devinApiKey = process.env.DEVIN_API_KEY;
153
+ const { report, runInfo, evidenceRoot } = await (0, detect_1.buildDriftReport)({
154
+ config,
155
+ repo,
156
+ baseSha: options.baseSha,
157
+ headSha: options.headSha,
158
+ trigger: options.trigger ?? "manual"
159
+ });
160
+ const docAreaByName = new Map(config.docAreas.map((area) => [area.name, area]));
161
+ let state = (0, state_1.loadState)();
162
+ const startedAt = Date.now();
163
+ const results = [];
164
+ const metrics = {
165
+ driftItemsDetected: report.items.length,
166
+ prsOpened: 0,
167
+ issuesOpened: 0,
168
+ blockedCount: 0,
169
+ timeToSessionTerminalMs: [],
170
+ docAreaCounts: {},
171
+ noiseRateProxy: 0
172
+ };
173
+ for (const item of report.items) {
174
+ metrics.docAreaCounts[item.docArea] = (metrics.docAreaCounts[item.docArea] ?? 0) + 1;
175
+ const areaConfig = docAreaByName.get(item.docArea);
176
+ if (!areaConfig) {
177
+ continue;
178
+ }
179
+ const decision = (0, engine_1.decidePolicy)({
180
+ item,
181
+ docAreaConfig: areaConfig,
182
+ config,
183
+ state,
184
+ repo,
185
+ baseSha: options.baseSha,
186
+ headSha: options.headSha
187
+ });
188
+ if (decision.action === "NOOP") {
189
+ results.push({
190
+ docArea: item.docArea,
191
+ decision,
192
+ outcome: "NO_CHANGE",
193
+ summary: decision.reason
194
+ });
195
+ continue;
196
+ }
197
+ if (decision.action === "UPDATE_EXISTING_PR") {
198
+ const existingPr = state.areaLatestPr[item.docArea];
199
+ const summary = existingPr
200
+ ? `Bundled into existing PR: ${existingPr}`
201
+ : "PR cap reached and no existing area PR; escalated";
202
+ const outcome = existingPr ? "NO_CHANGE" : "BLOCKED";
203
+ results.push({
204
+ docArea: item.docArea,
205
+ decision,
206
+ outcome,
207
+ summary,
208
+ prUrl: existingPr
209
+ });
210
+ state = (0, engine_1.applyDecisionToState)({
211
+ state,
212
+ decision,
213
+ docArea: item.docArea,
214
+ outcome,
215
+ link: existingPr
216
+ });
217
+ continue;
218
+ }
219
+ const bundle = await (0, bundle_1.buildEvidenceBundle)({ runInfo, item, evidenceRoot });
220
+ const attachmentPaths = [...new Set([bundle.archivePath, ...bundle.attachmentPaths])];
221
+ let sessionOutcome = {
222
+ outcome: "NO_CHANGE",
223
+ summary: "Skipped Devin session",
224
+ verification: config.policy.verification.commands.map((command) => ({ command, result: "not run" }))
225
+ };
226
+ if (devinApiKey) {
227
+ const sessionStart = Date.now();
228
+ sessionOutcome = await executeSession({
229
+ apiKey: devinApiKey,
230
+ repository: repo,
231
+ item,
232
+ attachmentPaths,
233
+ config
234
+ });
235
+ metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
236
+ }
237
+ else {
238
+ (0, log_1.logWarn)("DEVIN_API_KEY not set; running fallback behavior", { docArea: item.docArea });
239
+ sessionOutcome = {
240
+ outcome: "BLOCKED",
241
+ summary: "DEVIN_API_KEY missing; cannot start Devin session",
242
+ questions: ["Set DEVIN_API_KEY in environment or GitHub Actions secrets"],
243
+ verification: config.policy.verification.commands.map((command) => ({ command, result: "not run" }))
244
+ };
245
+ }
246
+ let issueUrl;
247
+ if (githubToken &&
248
+ (decision.action === "OPEN_ISSUE" || sessionOutcome.outcome === "BLOCKED" || sessionOutcome.outcome === "NO_CHANGE")) {
249
+ issueUrl = await (0, client_1.createIssue)({
250
+ token: githubToken,
251
+ repository: repo,
252
+ issue: {
253
+ title: `[docdrift] ${item.docArea}: docs drift requires input`,
254
+ body: (0, client_1.renderBlockedIssueBody)({
255
+ docArea: item.docArea,
256
+ evidenceSummary: item.summary,
257
+ questions: sessionOutcome.questions ?? ["Please confirm intended behavior and doc wording."],
258
+ sessionUrl: sessionOutcome.sessionUrl
259
+ }),
260
+ labels: ["docdrift"]
261
+ }
262
+ });
263
+ metrics.issuesOpened += 1;
264
+ sessionOutcome.outcome = "ISSUE_OPENED";
265
+ }
266
+ if (sessionOutcome.outcome === "PR_OPENED") {
267
+ metrics.prsOpened += 1;
268
+ }
269
+ if (sessionOutcome.outcome === "BLOCKED") {
270
+ metrics.blockedCount += 1;
271
+ }
272
+ const result = {
273
+ docArea: item.docArea,
274
+ decision,
275
+ outcome: sessionOutcome.outcome,
276
+ summary: sessionOutcome.summary,
277
+ sessionUrl: sessionOutcome.sessionUrl,
278
+ prUrl: sessionOutcome.prUrl,
279
+ issueUrl
280
+ };
281
+ results.push(result);
282
+ if (githubToken) {
283
+ const body = (0, client_1.renderRunComment)({
284
+ docArea: item.docArea,
285
+ summary: sessionOutcome.summary,
286
+ decision: decision.action,
287
+ outcome: sessionOutcome.outcome,
288
+ sessionUrl: sessionOutcome.sessionUrl,
289
+ prUrl: sessionOutcome.prUrl,
290
+ issueUrl,
291
+ validation: sessionOutcome.verification
292
+ });
293
+ await (0, client_1.postCommitComment)({
294
+ token: githubToken,
295
+ repository: repo,
296
+ commitSha,
297
+ body
298
+ });
299
+ }
300
+ state = (0, engine_1.applyDecisionToState)({
301
+ state,
302
+ decision,
303
+ docArea: item.docArea,
304
+ outcome: sessionOutcome.outcome,
305
+ link: sessionOutcome.prUrl ?? issueUrl
306
+ });
307
+ }
308
+ (0, state_1.saveState)(state);
309
+ metrics.noiseRateProxy =
310
+ metrics.driftItemsDetected === 0 ? 0 : Number((metrics.prsOpened / metrics.driftItemsDetected).toFixed(4));
311
+ (0, bundle_1.writeMetrics)(metrics);
312
+ (0, log_1.logInfo)("Run complete", {
313
+ items: report.items.length,
314
+ elapsedMs: Date.now() - startedAt
315
+ });
316
+ return results;
317
+ }
318
+ async function runValidate() {
319
+ const config = (0, load_1.loadConfig)();
320
+ const runtimeValidation = await (0, validate_1.validateRuntimeConfig)(config);
321
+ if (runtimeValidation.errors.length) {
322
+ throw new Error(`Config validation failed:\n${runtimeValidation.errors.join("\n")}`);
323
+ }
324
+ runtimeValidation.warnings.forEach((warning) => (0, log_1.logWarn)(warning));
325
+ (0, log_1.logInfo)("Config is valid");
326
+ }
327
+ async function runStatus(sinceHours = 24) {
328
+ const apiKey = process.env.DEVIN_API_KEY;
329
+ if (!apiKey) {
330
+ throw new Error("DEVIN_API_KEY is required for status command");
331
+ }
332
+ const sessions = await (0, v1_1.devinListSessions)(apiKey, { limit: 50, tag: "docdrift" });
333
+ const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
334
+ const filtered = sessions.filter((session) => {
335
+ const createdAt = session?.created_at ? Date.parse(String(session.created_at)) : Date.now();
336
+ return Number.isFinite(createdAt) ? createdAt >= cutoff : true;
337
+ });
338
+ if (!filtered.length) {
339
+ (0, log_1.logInfo)(`No docdrift sessions in last ${sinceHours}h`);
340
+ return;
341
+ }
342
+ for (const session of filtered) {
343
+ const id = String(session.session_id ?? session.id ?? "unknown");
344
+ const status = String(session.status_enum ?? session.status ?? "unknown");
345
+ const url = String(session.url ?? "");
346
+ console.log(`${id}\t${status}\t${url}`);
347
+ }
348
+ }
349
+ function resolveTrigger(eventName) {
350
+ if (eventName === "push") {
351
+ return "push";
352
+ }
353
+ if (eventName === "schedule") {
354
+ return "schedule";
355
+ }
356
+ return "manual";
357
+ }
358
+ function parseDurationHours(value) {
359
+ if (!value) {
360
+ return 24;
361
+ }
362
+ const normalized = value.endsWith("h") ? value.slice(0, -1) : value;
363
+ const parsed = Number(normalized);
364
+ if (!Number.isFinite(parsed) || parsed <= 0) {
365
+ throw new Error(`Invalid --since value: ${value}`);
366
+ }
367
+ return parsed;
368
+ }
369
+ function requireSha(value, label) {
370
+ if (!value) {
371
+ throw new Error(`Missing required argument: ${label}`);
372
+ }
373
+ return value;
374
+ }
375
+ exports.STATE_PATH = node_path_1.default.resolve(".docdrift", "state.json");
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emptyState = void 0;
4
+ const emptyState = () => ({
5
+ idempotency: {},
6
+ dailyPrCount: {},
7
+ areaDailyPrOpened: {},
8
+ areaLatestPr: {}
9
+ });
10
+ exports.emptyState = emptyState;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scoreSignals = scoreSignals;
4
+ exports.combineWithDevinPlan = combineWithDevinPlan;
5
+ const tierWeight = {
6
+ 0: 1,
7
+ 1: 0.9,
8
+ 2: 0.6,
9
+ 3: 0.35
10
+ };
11
+ function clamp01(value) {
12
+ return Math.max(0, Math.min(1, value));
13
+ }
14
+ function scoreSignals(signals) {
15
+ if (!signals.length) {
16
+ return 0;
17
+ }
18
+ let complement = 1;
19
+ for (const signal of signals) {
20
+ const weight = tierWeight[signal.tier] ?? 0.3;
21
+ const weighted = clamp01(signal.confidence * weight);
22
+ complement *= 1 - weighted;
23
+ }
24
+ return clamp01(1 - complement);
25
+ }
26
+ function combineWithDevinPlan(detectorConfidence, devinPlanConfidence) {
27
+ if (typeof devinPlanConfidence !== "number") {
28
+ return detectorConfidence;
29
+ }
30
+ return clamp01(detectorConfidence * 0.65 + devinPlanConfidence * 0.35);
31
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decidePolicy = decidePolicy;
4
+ exports.applyDecisionToState = applyDecisionToState;
5
+ const glob_1 = require("../utils/glob");
6
+ const hash_1 = require("../utils/hash");
7
+ const confidence_1 = require("./confidence");
8
+ function ymd(iso = new Date().toISOString()) {
9
+ return iso.slice(0, 10);
10
+ }
11
+ function buildIdempotencyKey(input) {
12
+ return (0, hash_1.sha256)(`${input.repo}:${input.docArea}:${input.baseSha}:${input.headSha}:${input.action}`);
13
+ }
14
+ function decidePolicy(input) {
15
+ const { item, docAreaConfig, config, state } = input;
16
+ const confidence = (0, confidence_1.scoreSignals)(item.signals);
17
+ const threshold = config.policy.confidence.autopatchThreshold;
18
+ const today = ymd();
19
+ const hasStrongSignal = item.signals.some((signal) => signal.tier <= 1);
20
+ const prCountToday = state.dailyPrCount[today] ?? 0;
21
+ const capReached = prCountToday >= config.policy.prCaps.maxPrsPerDay;
22
+ const areaDailyKey = `${today}:${item.docArea}`;
23
+ const exceedsFileCap = item.impactedDocs.length > config.policy.prCaps.maxFilesTouched;
24
+ const hasPathOutsideAllowlist = item.impactedDocs.some((filePath) => filePath && !(0, glob_1.isPathAllowed)(filePath, config.policy.allowlist));
25
+ let action = "NOOP";
26
+ let reason = "No action needed";
27
+ if (hasPathOutsideAllowlist) {
28
+ action = "OPEN_ISSUE";
29
+ reason = "Impacted files include non-allowlisted paths";
30
+ }
31
+ else if (exceedsFileCap) {
32
+ action = "OPEN_ISSUE";
33
+ reason = "Impacted files exceed maxFilesTouched policy cap";
34
+ }
35
+ else if (item.mode === "autogen") {
36
+ if (!hasStrongSignal) {
37
+ action = "OPEN_ISSUE";
38
+ reason = "Autogen area without strong signal; escalate as issue";
39
+ }
40
+ else if (confidence < threshold) {
41
+ action = "OPEN_ISSUE";
42
+ reason = `Confidence ${confidence.toFixed(2)} below threshold ${threshold.toFixed(2)}`;
43
+ }
44
+ else if (capReached) {
45
+ action = state.areaLatestPr[item.docArea] ? "UPDATE_EXISTING_PR" : "OPEN_ISSUE";
46
+ reason = "Daily PR cap reached";
47
+ }
48
+ else if (state.areaDailyPrOpened[areaDailyKey]) {
49
+ action = "UPDATE_EXISTING_PR";
50
+ reason = "One PR per doc area per day bundling rule";
51
+ }
52
+ else {
53
+ action = "OPEN_PR";
54
+ reason = "Strong autogen signal with confidence above threshold";
55
+ }
56
+ }
57
+ else {
58
+ const requireHuman = Boolean(docAreaConfig.patch.requireHumanConfirmation);
59
+ if (!requireHuman && hasStrongSignal && confidence >= Math.min(0.95, threshold + 0.1)) {
60
+ action = "OPEN_PR";
61
+ reason = "Conceptual area is high-confidence and human confirmation not required";
62
+ }
63
+ else {
64
+ action = "OPEN_ISSUE";
65
+ reason = "Conceptual drift defaults to human-in-the-loop issue";
66
+ }
67
+ }
68
+ const idempotencyKey = buildIdempotencyKey({
69
+ repo: input.repo,
70
+ docArea: item.docArea,
71
+ baseSha: input.baseSha,
72
+ headSha: input.headSha,
73
+ action
74
+ });
75
+ if (state.idempotency[idempotencyKey]) {
76
+ return {
77
+ action: "NOOP",
78
+ confidence,
79
+ reason: "Idempotency key already processed",
80
+ idempotencyKey
81
+ };
82
+ }
83
+ return {
84
+ action,
85
+ confidence,
86
+ reason,
87
+ idempotencyKey
88
+ };
89
+ }
90
+ function applyDecisionToState(input) {
91
+ const today = ymd();
92
+ const next = JSON.parse(JSON.stringify(input.state));
93
+ const record = {
94
+ createdAt: new Date().toISOString(),
95
+ action: input.decision.action,
96
+ outcome: input.outcome,
97
+ link: input.link
98
+ };
99
+ next.idempotency[input.decision.idempotencyKey] = record;
100
+ if (input.outcome === "PR_OPENED") {
101
+ next.dailyPrCount[today] = (next.dailyPrCount[today] ?? 0) + 1;
102
+ next.areaDailyPrOpened[`${today}:${input.docArea}`] = input.link ?? "opened";
103
+ if (input.link) {
104
+ next.areaLatestPr[input.docArea] = input.link;
105
+ }
106
+ }
107
+ return next;
108
+ }
@@ -0,0 +1,17 @@
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.loadState = loadState;
7
+ exports.saveState = saveState;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const state_1 = require("../model/state");
10
+ const fs_1 = require("../utils/fs");
11
+ const statePath = node_path_1.default.resolve(".docdrift", "state.json");
12
+ function loadState() {
13
+ return (0, fs_1.readJsonFile)(statePath, (0, state_1.emptyState)());
14
+ }
15
+ function saveState(state) {
16
+ (0, fs_1.writeJsonFile)(statePath, state);
17
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.execCommand = execCommand;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_util_1 = require("node:util");
6
+ const exec = (0, node_util_1.promisify)(node_child_process_1.exec);
7
+ async function execCommand(command, cwd = process.cwd()) {
8
+ try {
9
+ const { stdout, stderr } = await exec(command, {
10
+ cwd,
11
+ maxBuffer: 10 * 1024 * 1024
12
+ });
13
+ return { command, stdout, stderr, exitCode: 0 };
14
+ }
15
+ catch (error) {
16
+ const e = error;
17
+ return {
18
+ command,
19
+ stdout: e.stdout ?? "",
20
+ stderr: e.stderr ?? String(error),
21
+ exitCode: typeof e.code === "number" ? e.code : 1
22
+ };
23
+ }
24
+ }
@@ -0,0 +1,39 @@
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.ensureDir = ensureDir;
7
+ exports.readJsonFile = readJsonFile;
8
+ exports.writeJsonFile = writeJsonFile;
9
+ exports.safeReadText = safeReadText;
10
+ exports.copyIfExists = copyIfExists;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ function ensureDir(dirPath) {
14
+ node_fs_1.default.mkdirSync(dirPath, { recursive: true });
15
+ }
16
+ function readJsonFile(filePath, fallback) {
17
+ if (!node_fs_1.default.existsSync(filePath)) {
18
+ return fallback;
19
+ }
20
+ return JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8"));
21
+ }
22
+ function writeJsonFile(filePath, value) {
23
+ ensureDir(node_path_1.default.dirname(filePath));
24
+ node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
25
+ }
26
+ function safeReadText(filePath) {
27
+ if (!node_fs_1.default.existsSync(filePath)) {
28
+ return "";
29
+ }
30
+ return node_fs_1.default.readFileSync(filePath, "utf8");
31
+ }
32
+ function copyIfExists(sourcePath, destPath) {
33
+ if (!node_fs_1.default.existsSync(sourcePath)) {
34
+ return false;
35
+ }
36
+ ensureDir(node_path_1.default.dirname(destPath));
37
+ node_fs_1.default.copyFileSync(sourcePath, destPath);
38
+ return true;
39
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gitChangedPaths = gitChangedPaths;
4
+ exports.gitDiffSummary = gitDiffSummary;
5
+ exports.gitCommitList = gitCommitList;
6
+ const exec_1 = require("./exec");
7
+ async function gitChangedPaths(baseSha, headSha) {
8
+ const res = await (0, exec_1.execCommand)(`git diff --name-only ${baseSha} ${headSha}`);
9
+ if (res.exitCode !== 0) {
10
+ throw new Error(`Unable to compute changed paths: ${res.stderr}`);
11
+ }
12
+ return res.stdout
13
+ .split("\n")
14
+ .map((v) => v.trim())
15
+ .filter(Boolean);
16
+ }
17
+ async function gitDiffSummary(baseSha, headSha) {
18
+ const res = await (0, exec_1.execCommand)(`git diff --stat ${baseSha} ${headSha}`);
19
+ if (res.exitCode !== 0) {
20
+ throw new Error(`Unable to compute diff summary: ${res.stderr}`);
21
+ }
22
+ return res.stdout.trim();
23
+ }
24
+ async function gitCommitList(baseSha, headSha) {
25
+ const res = await (0, exec_1.execCommand)(`git log --pretty=format:%H ${baseSha}..${headSha}`);
26
+ if (res.exitCode !== 0) {
27
+ return [];
28
+ }
29
+ return res.stdout
30
+ .split("\n")
31
+ .map((v) => v.trim())
32
+ .filter(Boolean);
33
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.globToRegExp = globToRegExp;
4
+ exports.matchesGlob = matchesGlob;
5
+ exports.isPathAllowed = isPathAllowed;
6
+ function escapeRegex(input) {
7
+ return input.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
8
+ }
9
+ function globToRegExp(glob) {
10
+ const escaped = escapeRegex(glob)
11
+ .replace(/\*\*/g, "::DOUBLE_STAR::")
12
+ .replace(/\*/g, "[^/]*")
13
+ .replace(/::DOUBLE_STAR::/g, ".*");
14
+ return new RegExp(`^${escaped}$`);
15
+ }
16
+ function matchesGlob(glob, value) {
17
+ return globToRegExp(glob).test(value);
18
+ }
19
+ function isPathAllowed(path, allowlist) {
20
+ return allowlist.some((glob) => matchesGlob(glob, path));
21
+ }
@@ -0,0 +1,10 @@
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.sha256 = sha256;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ function sha256(input) {
9
+ return node_crypto_1.default.createHash("sha256").update(input).digest("hex");
10
+ }