@devinnn/docdrift 0.1.2 → 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/dist/src/cli.js +11 -9
- package/dist/src/config/normalize.js +93 -6
- package/dist/src/config/schema.js +78 -18
- package/dist/src/config/validate.js +6 -2
- package/dist/src/detect/index.js +76 -21
- package/dist/src/devin/prompts.js +45 -1
- package/dist/src/github/client.js +13 -0
- package/dist/src/index.js +68 -10
- 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/docdrift.schema.json +438 -0
- package/package.json +9 -4
package/dist/src/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ function getArg(args, flag) {
|
|
|
17
17
|
async function main() {
|
|
18
18
|
const [, , command, ...args] = process.argv;
|
|
19
19
|
if (!command) {
|
|
20
|
-
throw new Error("Usage: docdrift <validate|detect|run|status|sla-check> [options]");
|
|
20
|
+
throw new Error("Usage: docdrift <validate|detect|run|status|sla-check> [options]\n detect|run: [--base SHA] [--head SHA] (defaults: merge-base with main..HEAD)");
|
|
21
21
|
}
|
|
22
22
|
switch (command) {
|
|
23
23
|
case "validate": {
|
|
@@ -25,18 +25,20 @@ async function main() {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
case "detect": {
|
|
28
|
-
const baseSha = (0, index_1.
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
28
|
+
const { baseSha, headSha } = await (0, index_1.resolveBaseHead)(getArg(args, "--base"), getArg(args, "--head"));
|
|
29
|
+
const trigger = getArg(args, "--trigger") ?? (0, index_1.resolveTrigger)(process.env.GITHUB_EVENT_NAME);
|
|
30
|
+
const prNum = getArg(args, "--pr-number");
|
|
31
|
+
const prNumber = prNum ? parseInt(prNum, 10) : (process.env.GITHUB_PR_NUMBER ? parseInt(process.env.GITHUB_PR_NUMBER, 10) : undefined);
|
|
32
|
+
const result = await (0, index_1.runDetect)({ baseSha, headSha, trigger, prNumber: Number.isFinite(prNumber) ? prNumber : undefined });
|
|
32
33
|
process.exitCode = result.hasDrift ? 1 : 0;
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
case "run": {
|
|
36
|
-
const baseSha = (0, index_1.
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
37
|
+
const { baseSha, headSha } = await (0, index_1.resolveBaseHead)(getArg(args, "--base"), getArg(args, "--head"));
|
|
38
|
+
const trigger = getArg(args, "--trigger") ?? (0, index_1.resolveTrigger)(process.env.GITHUB_EVENT_NAME);
|
|
39
|
+
const prNum = getArg(args, "--pr-number");
|
|
40
|
+
const prNumber = prNum ? parseInt(prNum, 10) : (process.env.GITHUB_PR_NUMBER ? parseInt(process.env.GITHUB_PR_NUMBER, 10) : undefined);
|
|
41
|
+
const results = await (0, index_1.runDocDrift)({ baseSha, headSha, trigger, prNumber: Number.isFinite(prNumber) ? prNumber : undefined });
|
|
40
42
|
const outPath = node_path_1.default.resolve(".docdrift", "run-output.json");
|
|
41
43
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(outPath), { recursive: true });
|
|
42
44
|
node_fs_1.default.writeFileSync(outPath, JSON.stringify(results, null, 2), "utf-8");
|
|
@@ -7,25 +7,84 @@ exports.normalizeConfig = normalizeConfig;
|
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
/**
|
|
9
9
|
* Produce a normalized config that the rest of the app consumes.
|
|
10
|
-
* Derives openapi/docsite
|
|
10
|
+
* Derives specProviders/openapi/docsite from openapi block or docAreas when not using v2 specProviders.
|
|
11
11
|
*/
|
|
12
12
|
function normalizeConfig(config) {
|
|
13
|
+
let specProviders;
|
|
13
14
|
let openapi;
|
|
14
15
|
let docsite;
|
|
15
16
|
let exclude = config.exclude ?? [];
|
|
16
17
|
let requireHumanReview = config.requireHumanReview ?? [];
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
let docAreas = config.docAreas ?? [];
|
|
19
|
+
const allowConceptualOnlyRun = config.allowConceptualOnlyRun ?? false;
|
|
20
|
+
const inferMode = config.inferMode ?? true;
|
|
21
|
+
if (config.specProviders && config.specProviders.length >= 1) {
|
|
22
|
+
specProviders = config.specProviders;
|
|
23
|
+
const firstOpenApi3 = specProviders.find((p) => p.format === "openapi3");
|
|
24
|
+
if (firstOpenApi3 && firstOpenApi3.current.type === "export") {
|
|
25
|
+
openapi = {
|
|
26
|
+
export: firstOpenApi3.current.command,
|
|
27
|
+
generated: firstOpenApi3.current.outputPath,
|
|
28
|
+
published: firstOpenApi3.published,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
openapi = {
|
|
33
|
+
export: "echo",
|
|
34
|
+
generated: "",
|
|
35
|
+
published: specProviders[0].published,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const allPaths = specProviders.flatMap((p) => [p.published]);
|
|
39
|
+
const roots = new Set();
|
|
40
|
+
for (const p of allPaths) {
|
|
41
|
+
const parts = p.split("/").filter(Boolean);
|
|
42
|
+
if (parts.length >= 2)
|
|
43
|
+
roots.add(parts[0] + "/" + parts[1]);
|
|
44
|
+
else if (parts.length === 1)
|
|
45
|
+
roots.add(parts[0]);
|
|
46
|
+
}
|
|
47
|
+
docsite = roots.size > 0 ? [...roots] : config.docsite ? (Array.isArray(config.docsite) ? config.docsite : [config.docsite]) : ["."];
|
|
48
|
+
}
|
|
49
|
+
else if (config.openapi && config.docsite) {
|
|
50
|
+
specProviders = [
|
|
51
|
+
{
|
|
52
|
+
format: "openapi3",
|
|
53
|
+
current: { type: "export", command: config.openapi.export, outputPath: config.openapi.generated },
|
|
54
|
+
published: config.openapi.published,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
19
57
|
openapi = config.openapi;
|
|
20
58
|
docsite = Array.isArray(config.docsite) ? config.docsite : [config.docsite];
|
|
59
|
+
if (config.pathMappings?.length) {
|
|
60
|
+
const pathImpacts = new Set();
|
|
61
|
+
config.pathMappings.forEach((p) => p.impacts.forEach((i) => pathImpacts.add(i)));
|
|
62
|
+
requireHumanReview = [...new Set([...requireHumanReview, ...pathImpacts])];
|
|
63
|
+
docAreas = [
|
|
64
|
+
...docAreas,
|
|
65
|
+
{
|
|
66
|
+
name: "pathMappings",
|
|
67
|
+
mode: "conceptual",
|
|
68
|
+
owners: { reviewers: ["docdrift"] },
|
|
69
|
+
detect: { paths: config.pathMappings },
|
|
70
|
+
patch: { requireHumanConfirmation: true },
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
21
74
|
}
|
|
22
75
|
else if (config.docAreas && config.docAreas.length > 0) {
|
|
23
|
-
// Legacy: derive from docAreas
|
|
24
76
|
const firstOpenApiArea = config.docAreas.find((a) => a.detect.openapi);
|
|
25
77
|
if (!firstOpenApiArea?.detect.openapi) {
|
|
26
78
|
throw new Error("Legacy config requires at least one docArea with detect.openapi");
|
|
27
79
|
}
|
|
28
80
|
const o = firstOpenApiArea.detect.openapi;
|
|
81
|
+
specProviders = [
|
|
82
|
+
{
|
|
83
|
+
format: "openapi3",
|
|
84
|
+
current: { type: "export", command: o.exportCmd, outputPath: o.generatedPath },
|
|
85
|
+
published: o.publishedPath,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
29
88
|
openapi = {
|
|
30
89
|
export: o.exportCmd,
|
|
31
90
|
generated: o.generatedPath,
|
|
@@ -45,7 +104,6 @@ function normalizeConfig(config) {
|
|
|
45
104
|
roots.add(parts[0]);
|
|
46
105
|
}
|
|
47
106
|
docsite = roots.size > 0 ? [...roots] : [node_path_1.default.dirname(o.publishedPath) || "."];
|
|
48
|
-
// Derive requireHumanReview from areas with requireHumanConfirmation or conceptual mode
|
|
49
107
|
const reviewPaths = new Set();
|
|
50
108
|
for (const area of config.docAreas) {
|
|
51
109
|
if (area.patch.requireHumanConfirmation || area.mode === "conceptual") {
|
|
@@ -55,14 +113,43 @@ function normalizeConfig(config) {
|
|
|
55
113
|
}
|
|
56
114
|
requireHumanReview = [...reviewPaths];
|
|
57
115
|
}
|
|
116
|
+
else if (config.version === 2 && config.pathMappings?.length) {
|
|
117
|
+
specProviders = [];
|
|
118
|
+
openapi = { export: "echo", generated: "", published: "" };
|
|
119
|
+
const pathImpacts = new Set();
|
|
120
|
+
config.pathMappings.forEach((p) => p.impacts.forEach((i) => pathImpacts.add(i)));
|
|
121
|
+
requireHumanReview = [...new Set([...requireHumanReview, ...pathImpacts])];
|
|
122
|
+
docAreas = [
|
|
123
|
+
{
|
|
124
|
+
name: "pathMappings",
|
|
125
|
+
mode: "conceptual",
|
|
126
|
+
owners: { reviewers: ["docdrift"] },
|
|
127
|
+
detect: { paths: config.pathMappings },
|
|
128
|
+
patch: { requireHumanConfirmation: true },
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
const roots = new Set();
|
|
132
|
+
for (const p of pathImpacts) {
|
|
133
|
+
const parts = p.split("/").filter(Boolean);
|
|
134
|
+
if (parts.length >= 2)
|
|
135
|
+
roots.add(parts[0] + "/" + parts[1]);
|
|
136
|
+
else if (parts.length === 1)
|
|
137
|
+
roots.add(parts[0]);
|
|
138
|
+
}
|
|
139
|
+
docsite = roots.size > 0 ? [...roots] : config.docsite ? (Array.isArray(config.docsite) ? config.docsite : [config.docsite]) : ["."];
|
|
140
|
+
}
|
|
58
141
|
else {
|
|
59
|
-
throw new Error("Config must include (openapi + docsite) or docAreas");
|
|
142
|
+
throw new Error("Config must include specProviders, (openapi + docsite), or docAreas");
|
|
60
143
|
}
|
|
61
144
|
return {
|
|
62
145
|
...config,
|
|
146
|
+
specProviders,
|
|
63
147
|
openapi,
|
|
64
148
|
docsite,
|
|
65
149
|
exclude,
|
|
66
150
|
requireHumanReview,
|
|
151
|
+
docAreas,
|
|
152
|
+
allowConceptualOnlyRun,
|
|
153
|
+
inferMode,
|
|
67
154
|
};
|
|
68
155
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.docDriftConfigSchema = exports.openApiSimpleSchema = void 0;
|
|
3
|
+
exports.docDriftConfigSchema = exports.docDriftConfigBaseSchema = exports.docAreaBaseSchema = exports.specProviderConfigSchema = exports.openApiSimpleSchema = exports.pathRuleSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
|
|
5
|
+
/** Path rule: when `match` changes, `impacts` may need updates. Used by docAreas and by simple `paths` block. */
|
|
6
|
+
exports.pathRuleSchema = zod_1.z.object({
|
|
6
7
|
match: zod_1.z.string().min(1),
|
|
7
8
|
impacts: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
8
9
|
});
|
|
@@ -17,18 +18,47 @@ exports.openApiSimpleSchema = zod_1.z.object({
|
|
|
17
18
|
generated: zod_1.z.string().min(1),
|
|
18
19
|
published: zod_1.z.string().min(1),
|
|
19
20
|
});
|
|
21
|
+
/** Spec source: URL, local path, or export command */
|
|
22
|
+
const specSourceSchema = zod_1.z.discriminatedUnion("type", [
|
|
23
|
+
zod_1.z.object({ type: zod_1.z.literal("url"), url: zod_1.z.string().url() }),
|
|
24
|
+
zod_1.z.object({ type: zod_1.z.literal("local"), path: zod_1.z.string().min(1) }),
|
|
25
|
+
zod_1.z.object({
|
|
26
|
+
type: zod_1.z.literal("export"),
|
|
27
|
+
command: zod_1.z.string().min(1),
|
|
28
|
+
outputPath: zod_1.z.string().min(1),
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
const specFormatSchema = zod_1.z.enum(["openapi3", "swagger2", "graphql", "fern", "postman"]);
|
|
32
|
+
/** Single spec provider (v2 config) */
|
|
33
|
+
exports.specProviderConfigSchema = zod_1.z.object({
|
|
34
|
+
format: specFormatSchema,
|
|
35
|
+
current: specSourceSchema,
|
|
36
|
+
published: zod_1.z.string().min(1),
|
|
37
|
+
});
|
|
38
|
+
const docAreaDetectBaseSchema = zod_1.z.object({
|
|
39
|
+
openapi: openApiDetectSchema.optional(),
|
|
40
|
+
paths: zod_1.z.array(exports.pathRuleSchema).optional(),
|
|
41
|
+
});
|
|
42
|
+
/** Base schema without refine — for JSON Schema generation (zod-to-json-schema doesn't handle .refine()) */
|
|
43
|
+
exports.docAreaBaseSchema = zod_1.z.object({
|
|
44
|
+
name: zod_1.z.string().min(1),
|
|
45
|
+
mode: zod_1.z.enum(["autogen", "conceptual"]),
|
|
46
|
+
owners: zod_1.z.object({
|
|
47
|
+
reviewers: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
48
|
+
}),
|
|
49
|
+
detect: docAreaDetectBaseSchema,
|
|
50
|
+
patch: zod_1.z.object({
|
|
51
|
+
targets: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
52
|
+
requireHumanConfirmation: zod_1.z.boolean().optional().default(false),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
20
55
|
const docAreaSchema = zod_1.z.object({
|
|
21
56
|
name: zod_1.z.string().min(1),
|
|
22
57
|
mode: zod_1.z.enum(["autogen", "conceptual"]),
|
|
23
58
|
owners: zod_1.z.object({
|
|
24
59
|
reviewers: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
25
60
|
}),
|
|
26
|
-
detect:
|
|
27
|
-
.object({
|
|
28
|
-
openapi: openApiDetectSchema.optional(),
|
|
29
|
-
paths: zod_1.z.array(pathRuleSchema).optional(),
|
|
30
|
-
})
|
|
31
|
-
.refine((v) => Boolean(v.openapi) || Boolean(v.paths?.length), {
|
|
61
|
+
detect: docAreaDetectBaseSchema.refine((v) => Boolean(v.openapi) || Boolean(v.paths?.length), {
|
|
32
62
|
message: "docArea.detect must include openapi or paths",
|
|
33
63
|
}),
|
|
34
64
|
patch: zod_1.z.object({
|
|
@@ -59,17 +89,38 @@ const policySchema = zod_1.z.object({
|
|
|
59
89
|
*/
|
|
60
90
|
allowNewFiles: zod_1.z.boolean().optional().default(false),
|
|
61
91
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
version: zod_1.z.literal(1),
|
|
65
|
-
|
|
92
|
+
/** Base schema without refine — for JSON Schema generation (zod-to-json-schema doesn't handle .refine()) */
|
|
93
|
+
exports.docDriftConfigBaseSchema = zod_1.z.object({
|
|
94
|
+
version: zod_1.z.union([zod_1.z.literal(1), zod_1.z.literal(2)]),
|
|
95
|
+
specProviders: zod_1.z.array(exports.specProviderConfigSchema).optional(),
|
|
66
96
|
openapi: exports.openApiSimpleSchema.optional(),
|
|
67
|
-
/** Simple config: docsite root path(s) */
|
|
68
97
|
docsite: zod_1.z.union([zod_1.z.string().min(1), zod_1.z.array(zod_1.z.string().min(1))]).optional(),
|
|
69
|
-
/** Paths we never touch (glob patterns) */
|
|
70
98
|
exclude: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
71
|
-
/** Paths that require human review when touched (we create issue post-PR) */
|
|
72
99
|
requireHumanReview: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
100
|
+
pathMappings: zod_1.z.array(exports.pathRuleSchema).optional().default([]),
|
|
101
|
+
allowConceptualOnlyRun: zod_1.z.boolean().optional().default(false),
|
|
102
|
+
inferMode: zod_1.z.boolean().optional().default(true),
|
|
103
|
+
devin: zod_1.z.object({
|
|
104
|
+
apiVersion: zod_1.z.literal("v1"),
|
|
105
|
+
unlisted: zod_1.z.boolean().default(true),
|
|
106
|
+
maxAcuLimit: zod_1.z.number().int().positive().default(2),
|
|
107
|
+
tags: zod_1.z.array(zod_1.z.string().min(1)).default(["docdrift"]),
|
|
108
|
+
customInstructions: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
109
|
+
customInstructionContent: zod_1.z.string().optional(),
|
|
110
|
+
}),
|
|
111
|
+
policy: policySchema,
|
|
112
|
+
docAreas: zod_1.z.array(exports.docAreaBaseSchema).optional().default([]),
|
|
113
|
+
});
|
|
114
|
+
const docDriftConfigObjectSchema = zod_1.z.object({
|
|
115
|
+
version: zod_1.z.union([zod_1.z.literal(1), zod_1.z.literal(2)]),
|
|
116
|
+
specProviders: zod_1.z.array(exports.specProviderConfigSchema).optional(),
|
|
117
|
+
openapi: exports.openApiSimpleSchema.optional(),
|
|
118
|
+
docsite: zod_1.z.union([zod_1.z.string().min(1), zod_1.z.array(zod_1.z.string().min(1))]).optional(),
|
|
119
|
+
exclude: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
120
|
+
requireHumanReview: zod_1.z.array(zod_1.z.string().min(1)).optional().default([]),
|
|
121
|
+
pathMappings: zod_1.z.array(exports.pathRuleSchema).optional().default([]),
|
|
122
|
+
allowConceptualOnlyRun: zod_1.z.boolean().optional().default(false),
|
|
123
|
+
inferMode: zod_1.z.boolean().optional().default(true),
|
|
73
124
|
devin: zod_1.z.object({
|
|
74
125
|
apiVersion: zod_1.z.literal("v1"),
|
|
75
126
|
unlisted: zod_1.z.boolean().default(true),
|
|
@@ -79,7 +130,16 @@ exports.docDriftConfigSchema = zod_1.z
|
|
|
79
130
|
customInstructionContent: zod_1.z.string().optional(),
|
|
80
131
|
}),
|
|
81
132
|
policy: policySchema,
|
|
82
|
-
/** Legacy: doc areas (optional when openapi+docsite present) */
|
|
83
133
|
docAreas: zod_1.z.array(docAreaSchema).optional().default([]),
|
|
84
|
-
})
|
|
85
|
-
|
|
134
|
+
});
|
|
135
|
+
exports.docDriftConfigSchema = docDriftConfigObjectSchema.refine((v) => {
|
|
136
|
+
if (v.specProviders && v.specProviders.length >= 1)
|
|
137
|
+
return true;
|
|
138
|
+
if (v.openapi && v.docsite)
|
|
139
|
+
return true;
|
|
140
|
+
if (v.docAreas.length >= 1)
|
|
141
|
+
return true;
|
|
142
|
+
if (v.version === 2 && (v.pathMappings?.length ?? 0) >= 1)
|
|
143
|
+
return true;
|
|
144
|
+
return false;
|
|
145
|
+
}, { message: "Config must include specProviders, (openapi + docsite), docAreas, or (v2 + pathMappings)" });
|
|
@@ -11,11 +11,15 @@ async function validateRuntimeConfig(config) {
|
|
|
11
11
|
if (config.policy.prCaps.maxFilesTouched < 1) {
|
|
12
12
|
errors.push("policy.prCaps.maxFilesTouched must be >= 1");
|
|
13
13
|
}
|
|
14
|
-
const
|
|
14
|
+
const exportCommands = [
|
|
15
15
|
...config.policy.verification.commands,
|
|
16
16
|
...(config.openapi ? [config.openapi.export] : []),
|
|
17
17
|
...(config.docAreas ?? []).map((area) => area.detect.openapi?.exportCmd).filter((value) => Boolean(value)),
|
|
18
|
-
|
|
18
|
+
...(config.specProviders ?? [])
|
|
19
|
+
.filter((p) => p.current.type === "export")
|
|
20
|
+
.map((p) => p.current.command),
|
|
21
|
+
];
|
|
22
|
+
const commandSet = new Set(exportCommands);
|
|
19
23
|
for (const command of commandSet) {
|
|
20
24
|
const binary = commandBinary(command);
|
|
21
25
|
const result = await (0, exec_1.execCommand)(`command -v ${binary}`);
|
package/dist/src/detect/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
8
8
|
const fs_1 = require("../utils/fs");
|
|
9
9
|
const git_1 = require("../utils/git");
|
|
10
10
|
const heuristics_1 = require("./heuristics");
|
|
11
|
-
const
|
|
11
|
+
const registry_1 = require("../spec-providers/registry");
|
|
12
12
|
async function buildDriftReport(input) {
|
|
13
13
|
const runInfo = {
|
|
14
14
|
runId: `${Date.now()}`,
|
|
@@ -17,6 +17,7 @@ async function buildDriftReport(input) {
|
|
|
17
17
|
headSha: input.headSha,
|
|
18
18
|
trigger: input.trigger,
|
|
19
19
|
timestamp: new Date().toISOString(),
|
|
20
|
+
prNumber: input.prNumber,
|
|
20
21
|
};
|
|
21
22
|
const evidenceRoot = node_path_1.default.resolve(".docdrift", "evidence", runInfo.runId);
|
|
22
23
|
(0, fs_1.ensureDir)(evidenceRoot);
|
|
@@ -28,10 +29,76 @@ async function buildDriftReport(input) {
|
|
|
28
29
|
diffSummary,
|
|
29
30
|
commits,
|
|
30
31
|
});
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const { config } = input;
|
|
33
|
+
const signals = [];
|
|
34
|
+
const impactedDocs = new Set();
|
|
35
|
+
const summaries = [];
|
|
36
|
+
const evidenceFiles = [];
|
|
37
|
+
// 1. Run all spec providers (parallel)
|
|
38
|
+
const providerResults = [];
|
|
39
|
+
if (config.specProviders.length > 0) {
|
|
40
|
+
const results = await Promise.all(config.specProviders.map(async (provider) => {
|
|
41
|
+
const detector = (0, registry_1.getSpecDetector)(provider.format);
|
|
42
|
+
return detector(provider, evidenceRoot);
|
|
43
|
+
}));
|
|
44
|
+
providerResults.push(...results);
|
|
45
|
+
}
|
|
46
|
+
const anySpecDrift = providerResults.some((r) => r.hasDrift && r.signal && r.signal.tier <= 1);
|
|
47
|
+
const allSpecFailedOrNoDrift = providerResults.length === 0 ||
|
|
48
|
+
providerResults.every((r) => !r.hasDrift || (r.signal?.tier ?? 2) > 1);
|
|
49
|
+
if (anySpecDrift) {
|
|
50
|
+
for (const r of providerResults) {
|
|
51
|
+
if (r.hasDrift && r.signal) {
|
|
52
|
+
signals.push(r.signal);
|
|
53
|
+
r.impactedDocs.forEach((d) => impactedDocs.add(d));
|
|
54
|
+
summaries.push(r.summary);
|
|
55
|
+
evidenceFiles.push(...r.evidenceFiles);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// 2. Path heuristics (always run for aggregation when we have docAreas)
|
|
60
|
+
for (const docArea of config.docAreas) {
|
|
61
|
+
if (docArea.detect.paths?.length) {
|
|
62
|
+
const heuristicResult = (0, heuristics_1.detectHeuristicImpacts)(docArea, changedPaths, evidenceRoot);
|
|
63
|
+
if (heuristicResult.signal) {
|
|
64
|
+
signals.push(heuristicResult.signal);
|
|
65
|
+
heuristicResult.impactedDocs.forEach((d) => impactedDocs.add(d));
|
|
66
|
+
summaries.push(heuristicResult.summary);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const hasHeuristicMatch = signals.some((s) => s.kind === "heuristic_path_impact");
|
|
71
|
+
// 3. Gate logic
|
|
72
|
+
let runGate = "none";
|
|
73
|
+
if (anySpecDrift) {
|
|
74
|
+
runGate = "spec_drift";
|
|
75
|
+
}
|
|
76
|
+
else if (config.allowConceptualOnlyRun && hasHeuristicMatch) {
|
|
77
|
+
runGate = "conceptual_only";
|
|
78
|
+
}
|
|
79
|
+
else if (config.inferMode && (config.specProviders.length === 0 || allSpecFailedOrNoDrift)) {
|
|
80
|
+
runGate = "infer";
|
|
81
|
+
if (config.specProviders.length > 0) {
|
|
82
|
+
for (const r of providerResults) {
|
|
83
|
+
if (r.signal) {
|
|
84
|
+
signals.push(r.signal);
|
|
85
|
+
r.impactedDocs.forEach((d) => impactedDocs.add(d));
|
|
86
|
+
summaries.push(r.summary);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (signals.length === 0) {
|
|
91
|
+
signals.push({
|
|
92
|
+
kind: "infer_mode",
|
|
93
|
+
tier: 2,
|
|
94
|
+
confidence: 0.6,
|
|
95
|
+
evidence: [node_path_1.default.join(evidenceRoot, "changeset.json")],
|
|
96
|
+
});
|
|
97
|
+
changedPaths.forEach((p) => impactedDocs.add(p));
|
|
98
|
+
summaries.push("Infer mode: no spec drift; infer docs from file changes.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (runGate === "none") {
|
|
35
102
|
const report = {
|
|
36
103
|
run: {
|
|
37
104
|
repo: input.repo,
|
|
@@ -50,22 +117,9 @@ async function buildDriftReport(input) {
|
|
|
50
117
|
evidenceRoot,
|
|
51
118
|
runInfo,
|
|
52
119
|
hasOpenApiDrift: false,
|
|
120
|
+
runGate: "none",
|
|
53
121
|
};
|
|
54
122
|
}
|
|
55
|
-
// Gate passed: aggregate signals and impacted docs.
|
|
56
|
-
const signals = [openapiResult.signal];
|
|
57
|
-
const impactedDocs = new Set(openapiResult.impactedDocs);
|
|
58
|
-
const summaries = [openapiResult.summary];
|
|
59
|
-
for (const docArea of input.config.docAreas) {
|
|
60
|
-
if (docArea.detect.paths?.length) {
|
|
61
|
-
const heuristicResult = (0, heuristics_1.detectHeuristicImpacts)(docArea, changedPaths, evidenceRoot);
|
|
62
|
-
if (heuristicResult.signal) {
|
|
63
|
-
signals.push(heuristicResult.signal);
|
|
64
|
-
}
|
|
65
|
-
heuristicResult.impactedDocs.forEach((doc) => impactedDocs.add(doc));
|
|
66
|
-
summaries.push(heuristicResult.summary);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
123
|
const aggregated = {
|
|
70
124
|
signals,
|
|
71
125
|
impactedDocs: [...impactedDocs],
|
|
@@ -73,7 +127,7 @@ async function buildDriftReport(input) {
|
|
|
73
127
|
};
|
|
74
128
|
const item = {
|
|
75
129
|
docArea: "docsite",
|
|
76
|
-
mode: "autogen",
|
|
130
|
+
mode: runGate === "conceptual_only" ? "conceptual" : "autogen",
|
|
77
131
|
signals: aggregated.signals,
|
|
78
132
|
impactedDocs: aggregated.impactedDocs,
|
|
79
133
|
recommendedAction: aggregated.signals.some((s) => s.tier <= 1) ? "OPEN_PR" : "OPEN_ISSUE",
|
|
@@ -96,6 +150,7 @@ async function buildDriftReport(input) {
|
|
|
96
150
|
changedPaths,
|
|
97
151
|
evidenceRoot,
|
|
98
152
|
runInfo,
|
|
99
|
-
hasOpenApiDrift:
|
|
153
|
+
hasOpenApiDrift: anySpecDrift,
|
|
154
|
+
runGate,
|
|
100
155
|
};
|
|
101
156
|
}
|
|
@@ -74,9 +74,52 @@ function buildWholeDocsitePrompt(input) {
|
|
|
74
74
|
const newFilesRule = allowNewFiles
|
|
75
75
|
? "8) You MAY add new articles, create new folders, and change information architecture when warranted."
|
|
76
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 driftSummary = input.aggregated.summary?.trim();
|
|
78
|
+
const openapiPublished = input.config.openapi?.published;
|
|
79
|
+
const openapiGenerated = input.config.openapi?.generated;
|
|
80
|
+
const specLine = openapiPublished && openapiGenerated
|
|
81
|
+
? `Update ${openapiPublished} to match the generated spec (${openapiGenerated}). The attachments contain the full diff.`
|
|
82
|
+
: "Update published docs to match the evidence (attachments).";
|
|
83
|
+
const driftBlock = driftSummary &&
|
|
84
|
+
[
|
|
85
|
+
"DRIFT DETECTED (you must fix this):",
|
|
86
|
+
"---",
|
|
87
|
+
driftSummary,
|
|
88
|
+
"---",
|
|
89
|
+
specLine,
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
const inferBlock = input.runGate === "infer"
|
|
93
|
+
? [
|
|
94
|
+
"INFER MODE: No API spec diff was available. These file changes may impact docs.",
|
|
95
|
+
"Infer what documentation might need updates from the changed files. Update or create docs as needed.",
|
|
96
|
+
"Do NOT invent APIs; only document what you can infer from the code changes.",
|
|
97
|
+
"",
|
|
98
|
+
].join("\n")
|
|
99
|
+
: "";
|
|
100
|
+
const draftPrBlock = input.trigger === "pull_request" && input.prNumber
|
|
101
|
+
? [
|
|
102
|
+
"",
|
|
103
|
+
"This run was triggered by an open API PR. Open a **draft** pull request.",
|
|
104
|
+
`In the PR description, link to the API PR (#${input.prNumber}) and state: "Merge the API PR first, then review this doc PR."`,
|
|
105
|
+
"Use a branch name like docdrift/pr-" + input.prNumber + " or docdrift/preview-<short-sha>.",
|
|
106
|
+
"",
|
|
107
|
+
].join("\n")
|
|
108
|
+
: "";
|
|
109
|
+
const pathMappings = input.config.pathMappings ?? [];
|
|
110
|
+
const pathMappingsBlock = pathMappings.length > 0
|
|
111
|
+
? [
|
|
112
|
+
"PATH MAPPINGS (when these code paths change, consider these docs for updates):",
|
|
113
|
+
...pathMappings.map((p) => `- ${p.match} → ${p.impacts.join(", ")}`),
|
|
114
|
+
"",
|
|
115
|
+
].join("\n")
|
|
116
|
+
: "";
|
|
77
117
|
const base = [
|
|
78
118
|
"You are Devin. Task: update the entire docsite to match the API and code changes.",
|
|
79
119
|
"",
|
|
120
|
+
driftBlock ?? "",
|
|
121
|
+
inferBlock,
|
|
122
|
+
pathMappingsBlock,
|
|
80
123
|
"EVIDENCE (attachments):",
|
|
81
124
|
input.attachmentUrls.map((url, i) => `- ATTACHMENT ${i + 1}: ${url}`).join("\n"),
|
|
82
125
|
"",
|
|
@@ -86,7 +129,8 @@ function buildWholeDocsitePrompt(input) {
|
|
|
86
129
|
"3) Update API reference (OpenAPI) and any impacted guides in one PR.",
|
|
87
130
|
"4) Run verification commands and record results:",
|
|
88
131
|
...input.config.policy.verification.commands.map((c) => ` - ${c}`),
|
|
89
|
-
"5) Open exactly ONE pull request with a clear title and reviewer-friendly description."
|
|
132
|
+
"5) Open exactly ONE pull request with a clear title and reviewer-friendly description." +
|
|
133
|
+
draftPrBlock,
|
|
90
134
|
`6) Docsite scope: ${input.config.docsite.join(", ")}` +
|
|
91
135
|
excludeNote +
|
|
92
136
|
requireReviewNote +
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseRepo = parseRepo;
|
|
4
4
|
exports.postCommitComment = postCommitComment;
|
|
5
|
+
exports.postPrComment = postPrComment;
|
|
5
6
|
exports.createIssue = createIssue;
|
|
6
7
|
exports.renderRunComment = renderRunComment;
|
|
7
8
|
exports.renderBlockedIssueBody = renderBlockedIssueBody;
|
|
@@ -28,6 +29,18 @@ async function postCommitComment(input) {
|
|
|
28
29
|
});
|
|
29
30
|
return response.data.html_url;
|
|
30
31
|
}
|
|
32
|
+
/** Post a comment on a pull request (e.g. to link the doc drift PR when trigger is pull_request). */
|
|
33
|
+
async function postPrComment(input) {
|
|
34
|
+
const octokit = new rest_1.Octokit({ auth: input.token });
|
|
35
|
+
const { owner, repo } = parseRepo(input.repository);
|
|
36
|
+
const response = await octokit.issues.createComment({
|
|
37
|
+
owner,
|
|
38
|
+
repo,
|
|
39
|
+
issue_number: input.prNumber,
|
|
40
|
+
body: input.body,
|
|
41
|
+
});
|
|
42
|
+
return response.data.html_url;
|
|
43
|
+
}
|
|
31
44
|
async function createIssue(input) {
|
|
32
45
|
const octokit = new rest_1.Octokit({ auth: input.token });
|
|
33
46
|
const { owner, repo } = parseRepo(input.repository);
|