@devinnn/docdrift 0.1.0 → 0.1.1
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 +62 -12
- package/dist/src/cli.js +8 -0
- package/dist/src/config/load.js +14 -1
- package/dist/src/config/schema.js +15 -13
- package/dist/src/config/validate.js +1 -1
- package/dist/src/detect/docsCheck.js +4 -4
- package/dist/src/detect/heuristics.js +2 -2
- package/dist/src/detect/index.js +5 -5
- package/dist/src/detect/openapi.js +9 -9
- package/dist/src/devin/prompts.js +13 -5
- package/dist/src/devin/schemas.js +19 -19
- package/dist/src/devin/v1.js +8 -8
- package/dist/src/evidence/bundle.js +3 -3
- package/dist/src/github/client.js +2 -2
- package/dist/src/index.js +45 -31
- package/dist/src/model/state.js +1 -1
- package/dist/src/policy/confidence.js +1 -1
- package/dist/src/policy/engine.js +4 -4
- package/dist/src/utils/exec.js +2 -2
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Docs that never lie: detect drift between merged code and docs, then open low-noise, evidence-grounded remediation via Devin sessions.
|
|
4
4
|
|
|
5
5
|
## Deliverables
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
- **npm package**: [@devinnn/docdrift](https://www.npmjs.com/package/@devinnn/docdrift) — TypeScript CLI (`docdrift`)
|
|
7
8
|
- `validate`
|
|
8
9
|
- `detect --base <sha> --head <sha>`
|
|
9
10
|
- `run --base <sha> --head <sha>`
|
|
@@ -14,6 +15,7 @@ Docs that never lie: detect drift between merged code and docs, then open low-no
|
|
|
14
15
|
- PR template + Loom script
|
|
15
16
|
|
|
16
17
|
## Why this is low-noise
|
|
18
|
+
|
|
17
19
|
- One PR per doc area per day (bundling rule).
|
|
18
20
|
- Global PR/day cap.
|
|
19
21
|
- Confidence gating and allowlist enforcement.
|
|
@@ -21,17 +23,20 @@ Docs that never lie: detect drift between merged code and docs, then open low-no
|
|
|
21
23
|
- Idempotency key prevents duplicate actions for same repo/SHAs/action.
|
|
22
24
|
|
|
23
25
|
## Detection tiers
|
|
24
|
-
|
|
25
|
-
- Tier
|
|
26
|
-
- Tier
|
|
26
|
+
|
|
27
|
+
- Tier 0: docsite verification (`npm run docs:gen` then `npm run docs:build`)
|
|
28
|
+
- Tier 1: OpenAPI drift (`openapi/generated.json` vs `apps/docs-site/openapi/openapi.json`)
|
|
29
|
+
- Tier 2: heuristic path impacts (e.g. `apps/api/src/auth/**` -> `apps/docs-site/docs/guides/auth.md`)
|
|
27
30
|
|
|
28
31
|
Output artifacts (under `.docdrift/`):
|
|
32
|
+
|
|
29
33
|
- `drift_report.json`
|
|
30
34
|
- `metrics.json` (after `run`)
|
|
31
35
|
|
|
32
36
|
When you run docdrift as a package (e.g. `npx docdrift` or from another repo), all of this is written to **that repo’s** `.docdrift/` — i.e. the current working directory where the CLI is invoked, not inside the package. Add `.docdrift/` to the consuming repo’s `.gitignore` if you don’t want to commit run artifacts.
|
|
33
37
|
|
|
34
38
|
## Core flow (`docdrift run`)
|
|
39
|
+
|
|
35
40
|
1. Validate config and command availability.
|
|
36
41
|
2. Build drift report.
|
|
37
42
|
3. Policy decision (`OPEN_PR | UPDATE_EXISTING_PR | OPEN_ISSUE | NOOP`).
|
|
@@ -41,7 +46,26 @@ When you run docdrift as a package (e.g. `npx docdrift` or from another repo), a
|
|
|
41
46
|
7. Surface result via GitHub commit comment; open issue on blocked/low-confidence paths.
|
|
42
47
|
8. Persist state in `.docdrift/state.json` and write `.docdrift/metrics.json`.
|
|
43
48
|
|
|
49
|
+
## Where the docs are (this repo)
|
|
50
|
+
|
|
51
|
+
| Path | Purpose |
|
|
52
|
+
| ------------------------------------------ | ----------------------------------------------------------------------- |
|
|
53
|
+
| `apps/docs-site/openapi/openapi.json` | Published OpenAPI spec (docdrift updates this when drift is detected). |
|
|
54
|
+
| `apps/docs-site/docs/api/` | API reference MDX generated from the spec (`npm run docs:gen`). |
|
|
55
|
+
| `apps/docs-site/docs/guides/auth.md` | Conceptual auth guide (updated only for conceptual drift). |
|
|
56
|
+
|
|
57
|
+
The docsite is a Docusaurus app with `docusaurus-plugin-openapi-docs`. The **generated** spec from code lives at `openapi/generated.json` (from `npm run openapi:export`). Drift = generated vs published differ. Verification runs `docs:gen` and `docs:build` so the docsite actually builds.
|
|
58
|
+
|
|
59
|
+
## How Devin updates them
|
|
60
|
+
|
|
61
|
+
1. **Evidence bundle** — Docdrift builds a tarball with the drift report, OpenAPI diff, and impacted doc snippets, and uploads it to the Devin API as session attachments.
|
|
62
|
+
2. **Devin session** — Devin is prompted (see `src/devin/prompts.ts`) to update only files under the allowlist (`openapi/**`, `apps/docs-site/**`), make minimal correct edits, run verification (`npm run docs:gen`, `npm run docs:build`), and open **one PR** per doc area with a clear description.
|
|
63
|
+
3. **PR** — Devin updates `apps/docs-site/openapi/openapi.json` to match the current API, runs `docs:gen` to regenerate API reference MDX, and opens a pull request. You review and merge; the docsite builds and the docs are updated.
|
|
64
|
+
|
|
65
|
+
So the “fix” is a **PR opened by Devin** that you merge; the repo’s docs don’t change until that PR is merged.
|
|
66
|
+
|
|
44
67
|
## Local usage
|
|
68
|
+
|
|
45
69
|
```bash
|
|
46
70
|
npm install
|
|
47
71
|
npx tsx src/cli.ts validate
|
|
@@ -54,25 +78,29 @@ DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha>
|
|
|
54
78
|
|
|
55
79
|
You can run a full end-to-end demo locally with no remote repo. Ensure `.env` has `DEVIN_API_KEY` (and optionally `GITHUB_TOKEN` only when you have a real repo).
|
|
56
80
|
|
|
57
|
-
1. **One-time setup (already done if you have two commits with drift)**
|
|
58
|
-
- Git is inited; baseline commit has docs in sync with API.
|
|
81
|
+
1. **One-time setup (already done if you have two commits with drift)**
|
|
82
|
+
- Git is inited; baseline commit has docs in sync with API.
|
|
59
83
|
- A later commit changes `apps/api/src/model.ts` (e.g. `name` → `fullName`) and runs `npm run openapi:export`, so `openapi/generated.json` drifts from `docs/reference/openapi.json`.
|
|
60
84
|
|
|
61
85
|
2. **Run the pipeline**
|
|
86
|
+
|
|
62
87
|
```bash
|
|
63
88
|
npm install
|
|
64
89
|
npx tsx src/cli.ts validate
|
|
65
90
|
npx tsx src/cli.ts detect --base b0f624f --head 6030902
|
|
66
91
|
```
|
|
92
|
+
|
|
67
93
|
- Use your own `git log --oneline -3` to get `base` (older) and `head` (newer) SHAs if you recreated the demo.
|
|
68
94
|
|
|
69
95
|
3. **Run with Devin (no GitHub calls)**
|
|
70
96
|
Omit `GITHUB_TOKEN` so the CLI does not post comments or create issues. Devin session still runs; results are printed to stdout and written to `.docdrift/state.json` and `metrics.json`.
|
|
97
|
+
|
|
71
98
|
```bash
|
|
72
99
|
export $(grep -v '^#' .env | xargs)
|
|
73
100
|
unset GITHUB_TOKEN GITHUB_REPOSITORY GITHUB_SHA
|
|
74
101
|
npx tsx src/cli.ts run --base b0f624f --head 6030902
|
|
75
102
|
```
|
|
103
|
+
|
|
76
104
|
- `run` can take 1–3 minutes while the Devin session runs.
|
|
77
105
|
|
|
78
106
|
4. **What you’ll see**
|
|
@@ -93,14 +121,15 @@ You can run a full end-to-end demo locally with no remote repo. Ensure `.env` ha
|
|
|
93
121
|
## Run on GitHub
|
|
94
122
|
|
|
95
123
|
1. **Create a repo** on GitHub (e.g. `your-org/docdrift`), then add the remote and push:
|
|
124
|
+
|
|
96
125
|
```bash
|
|
97
126
|
git remote add origin https://github.com/your-org/docdrift.git
|
|
98
127
|
git push -u origin main
|
|
99
128
|
```
|
|
100
129
|
|
|
101
130
|
2. **Add secret**
|
|
102
|
-
Repo → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**
|
|
103
|
-
- Name: `DEVIN_API_KEY`
|
|
131
|
+
Repo → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**
|
|
132
|
+
- Name: `DEVIN_API_KEY`
|
|
104
133
|
- Value: your Devin API key (same as in `.env` locally)
|
|
105
134
|
|
|
106
135
|
`GITHUB_TOKEN` is provided automatically; the workflow uses it for commit comments and issues.
|
|
@@ -109,6 +138,25 @@ You can run a full end-to-end demo locally with no remote repo. Ensure `.env` ha
|
|
|
109
138
|
- **Push to `main`** — runs on every push (compares previous commit vs current).
|
|
110
139
|
- **Manual run** — **Actions** tab → **devin-doc-drift** → **Run workflow** (uses `HEAD` and `HEAD^` as head/base).
|
|
111
140
|
|
|
141
|
+
## See it work (demo on GitHub)
|
|
142
|
+
|
|
143
|
+
This repo has **intentional drift**: the API has been expanded (new fields `fullName`, `avatarUrl`, `createdAt`, `role` and new endpoint `GET /v1/users` with pagination), but **docs are unchanged** (`docs/reference/openapi.json` and `docs/reference/api.md` still describe the old single-endpoint, `id`/`name`/`email` only). Running docdrift will detect that and hand a large diff to Devin to fix via a PR. To see it:
|
|
144
|
+
|
|
145
|
+
1. **Create a new GitHub repo** (e.g. `docdrift-demo`) so you have a clean place to run the workflow.
|
|
146
|
+
2. **Push this project with full history** (so both commits are on `main`):
|
|
147
|
+
```bash
|
|
148
|
+
git remote add origin https://github.com/YOUR_ORG/docdrift-demo.git
|
|
149
|
+
git push -u origin main
|
|
150
|
+
```
|
|
151
|
+
3. **Add secret** in that repo: **Settings** → **Secrets and variables** → **Actions** → `DEVIN_API_KEY` = your Devin API key.
|
|
152
|
+
4. **Trigger the workflow**
|
|
153
|
+
- Either push another small commit (e.g. README tweak), or
|
|
154
|
+
- **Actions** → **devin-doc-drift** → **Run workflow**.
|
|
155
|
+
5. **Where to look**
|
|
156
|
+
- **Actions** → open the run → **Run Doc Drift** step: the step logs print JSON with `sessionUrl`, `prUrl`, and `outcome` per doc area. Open any `sessionUrl` in your browser to see the Devin session.
|
|
157
|
+
- **Artifacts**: download **docdrift-artifacts** for `.docdrift/drift_report.json`, `.docdrift/metrics.json`, and evidence.
|
|
158
|
+
- **Devin dashboard**: sessions are tagged `docdrift`; you’ll see the run there once the step completes (often 1–3 minutes).
|
|
159
|
+
|
|
112
160
|
## Using in another repo (published package)
|
|
113
161
|
|
|
114
162
|
Once published to npm, any repo can use the CLI locally or in GitHub Actions.
|
|
@@ -116,14 +164,14 @@ Once published to npm, any repo can use the CLI locally or in GitHub Actions.
|
|
|
116
164
|
1. **In the consuming repo** add a `docdrift.yaml` at the root (see this repo’s `docdrift.yaml` and `docdrift-yml.md`).
|
|
117
165
|
2. **CLI**
|
|
118
166
|
```bash
|
|
119
|
-
npx docdrift
|
|
120
|
-
npx docdrift
|
|
167
|
+
npx @devinnn/docdrift validate
|
|
168
|
+
npx @devinnn/docdrift detect --base <base-sha> --head <head-sha>
|
|
121
169
|
# With env for run:
|
|
122
|
-
DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha> npx docdrift
|
|
170
|
+
DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha> npx @devinnn/docdrift run --base <base-sha> --head <head-sha>
|
|
123
171
|
```
|
|
124
172
|
3. **GitHub Actions** — add a step that runs the CLI (e.g. after checkout and setting base/head):
|
|
125
173
|
```yaml
|
|
126
|
-
- run: npx docdrift
|
|
174
|
+
- run: npx @devinnn/docdrift run --base ${{ steps.shas.outputs.base }} --head ${{ steps.shas.outputs.head }}
|
|
127
175
|
env:
|
|
128
176
|
DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
|
|
129
177
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -140,8 +188,10 @@ Once published to npm, any repo can use the CLI locally or in GitHub Actions.
|
|
|
140
188
|
- Only the `dist/` directory is included (`files` in `package.json`). Consumers get the built CLI; they provide their own `docdrift.yaml` in their repo.
|
|
141
189
|
|
|
142
190
|
## Demo scenario
|
|
191
|
+
|
|
143
192
|
- Autogen drift: rename a field in `apps/api/src/model.ts`, merge to `main`, observe docs PR path.
|
|
144
193
|
- Conceptual drift: change auth behavior under `apps/api/src/auth/**`, merge to `main`, observe single escalation issue.
|
|
145
194
|
|
|
146
195
|
## Loom
|
|
196
|
+
|
|
147
197
|
See `/Users/cameronking/Desktop/sideproject/docdrift/loom.md` for the minute-by-minute recording script.
|
package/dist/src/cli.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
4
9
|
const index_1 = require("./index");
|
|
5
10
|
function getArg(args, flag) {
|
|
6
11
|
const index = args.indexOf(flag);
|
|
@@ -32,6 +37,9 @@ async function main() {
|
|
|
32
37
|
const headSha = (0, index_1.requireSha)(getArg(args, "--head"), "--head");
|
|
33
38
|
const trigger = (0, index_1.resolveTrigger)(process.env.GITHUB_EVENT_NAME);
|
|
34
39
|
const results = await (0, index_1.runDocDrift)({ baseSha, headSha, trigger });
|
|
40
|
+
const outPath = node_path_1.default.resolve(".docdrift", "run-output.json");
|
|
41
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(outPath), { recursive: true });
|
|
42
|
+
node_fs_1.default.writeFileSync(outPath, JSON.stringify(results, null, 2), "utf-8");
|
|
35
43
|
console.log(JSON.stringify(results, null, 2));
|
|
36
44
|
return;
|
|
37
45
|
}
|
package/dist/src/config/load.js
CHANGED
|
@@ -21,5 +21,18 @@ function loadConfig(configPath = "docdrift.yaml") {
|
|
|
21
21
|
.join("\n");
|
|
22
22
|
throw new Error(`Invalid config:\n${message}`);
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
const data = result.data;
|
|
25
|
+
if (data.devin.customInstructions?.length) {
|
|
26
|
+
const configDir = node_path_1.default.dirname(resolved);
|
|
27
|
+
const contents = [];
|
|
28
|
+
for (const p of data.devin.customInstructions) {
|
|
29
|
+
const fullPath = node_path_1.default.resolve(configDir, p);
|
|
30
|
+
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
31
|
+
throw new Error(`Custom instructions file not found: ${fullPath}`);
|
|
32
|
+
}
|
|
33
|
+
contents.push(node_fs_1.default.readFileSync(fullPath, "utf8"));
|
|
34
|
+
}
|
|
35
|
+
data.devin.customInstructionContent = contents.join("\n\n");
|
|
36
|
+
}
|
|
37
|
+
return data;
|
|
25
38
|
}
|
|
@@ -4,31 +4,31 @@ exports.docDriftConfigSchema = void 0;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const pathRuleSchema = zod_1.z.object({
|
|
6
6
|
match: zod_1.z.string().min(1),
|
|
7
|
-
impacts: zod_1.z.array(zod_1.z.string().min(1)).min(1)
|
|
7
|
+
impacts: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
8
8
|
});
|
|
9
9
|
const openApiDetectSchema = zod_1.z.object({
|
|
10
10
|
exportCmd: zod_1.z.string().min(1),
|
|
11
11
|
generatedPath: zod_1.z.string().min(1),
|
|
12
|
-
publishedPath: zod_1.z.string().min(1)
|
|
12
|
+
publishedPath: zod_1.z.string().min(1),
|
|
13
13
|
});
|
|
14
14
|
const docAreaSchema = zod_1.z.object({
|
|
15
15
|
name: zod_1.z.string().min(1),
|
|
16
16
|
mode: zod_1.z.enum(["autogen", "conceptual"]),
|
|
17
17
|
owners: zod_1.z.object({
|
|
18
|
-
reviewers: zod_1.z.array(zod_1.z.string().min(1)).min(1)
|
|
18
|
+
reviewers: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
19
19
|
}),
|
|
20
20
|
detect: zod_1.z
|
|
21
21
|
.object({
|
|
22
22
|
openapi: openApiDetectSchema.optional(),
|
|
23
|
-
paths: zod_1.z.array(pathRuleSchema).optional()
|
|
23
|
+
paths: zod_1.z.array(pathRuleSchema).optional(),
|
|
24
24
|
})
|
|
25
25
|
.refine((v) => Boolean(v.openapi) || Boolean(v.paths?.length), {
|
|
26
|
-
message: "docArea.detect must include openapi or paths"
|
|
26
|
+
message: "docArea.detect must include openapi or paths",
|
|
27
27
|
}),
|
|
28
28
|
patch: zod_1.z.object({
|
|
29
29
|
targets: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
30
|
-
requireHumanConfirmation: zod_1.z.boolean().optional().default(false)
|
|
31
|
-
})
|
|
30
|
+
requireHumanConfirmation: zod_1.z.boolean().optional().default(false),
|
|
31
|
+
}),
|
|
32
32
|
});
|
|
33
33
|
exports.docDriftConfigSchema = zod_1.z.object({
|
|
34
34
|
version: zod_1.z.literal(1),
|
|
@@ -36,20 +36,22 @@ exports.docDriftConfigSchema = zod_1.z.object({
|
|
|
36
36
|
apiVersion: zod_1.z.literal("v1"),
|
|
37
37
|
unlisted: zod_1.z.boolean().default(true),
|
|
38
38
|
maxAcuLimit: zod_1.z.number().int().positive().default(2),
|
|
39
|
-
tags: zod_1.z.array(zod_1.z.string().min(1)).default(["docdrift"])
|
|
39
|
+
tags: zod_1.z.array(zod_1.z.string().min(1)).default(["docdrift"]),
|
|
40
|
+
customInstructions: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
41
|
+
customInstructionContent: zod_1.z.string().optional(),
|
|
40
42
|
}),
|
|
41
43
|
policy: zod_1.z.object({
|
|
42
44
|
prCaps: zod_1.z.object({
|
|
43
45
|
maxPrsPerDay: zod_1.z.number().int().positive().default(1),
|
|
44
|
-
maxFilesTouched: zod_1.z.number().int().positive().default(12)
|
|
46
|
+
maxFilesTouched: zod_1.z.number().int().positive().default(12),
|
|
45
47
|
}),
|
|
46
48
|
confidence: zod_1.z.object({
|
|
47
|
-
autopatchThreshold: zod_1.z.number().min(0).max(1).default(0.8)
|
|
49
|
+
autopatchThreshold: zod_1.z.number().min(0).max(1).default(0.8),
|
|
48
50
|
}),
|
|
49
51
|
allowlist: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
50
52
|
verification: zod_1.z.object({
|
|
51
|
-
commands: zod_1.z.array(zod_1.z.string().min(1)).min(1)
|
|
52
|
-
})
|
|
53
|
+
commands: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
54
|
+
}),
|
|
53
55
|
}),
|
|
54
|
-
docAreas: zod_1.z.array(docAreaSchema).min(1)
|
|
56
|
+
docAreas: zod_1.z.array(docAreaSchema).min(1),
|
|
55
57
|
});
|
|
@@ -15,7 +15,7 @@ async function validateRuntimeConfig(config) {
|
|
|
15
15
|
...config.policy.verification.commands,
|
|
16
16
|
...config.docAreas
|
|
17
17
|
.map((area) => area.detect.openapi?.exportCmd)
|
|
18
|
-
.filter((value) => Boolean(value))
|
|
18
|
+
.filter((value) => Boolean(value)),
|
|
19
19
|
]);
|
|
20
20
|
for (const command of commandSet) {
|
|
21
21
|
const binary = commandBinary(command);
|
|
@@ -21,7 +21,7 @@ async function runDocsChecks(commands, evidenceDir) {
|
|
|
21
21
|
"\n--- stdout ---",
|
|
22
22
|
result.stdout,
|
|
23
23
|
"\n--- stderr ---",
|
|
24
|
-
result.stderr
|
|
24
|
+
result.stderr,
|
|
25
25
|
].join("\n"), "utf8");
|
|
26
26
|
logs.push(logPath);
|
|
27
27
|
commandResults.push({ command, exitCode: result.exitCode, logPath });
|
|
@@ -31,7 +31,7 @@ async function runDocsChecks(commands, evidenceDir) {
|
|
|
31
31
|
return {
|
|
32
32
|
logs,
|
|
33
33
|
commandResults,
|
|
34
|
-
summary: "Docs checks passed"
|
|
34
|
+
summary: "Docs checks passed",
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
@@ -42,7 +42,7 @@ async function runDocsChecks(commands, evidenceDir) {
|
|
|
42
42
|
kind: "docs_check_failed",
|
|
43
43
|
tier: 0,
|
|
44
44
|
confidence: 0.99,
|
|
45
|
-
evidence: failed.map((result) => result.logPath)
|
|
46
|
-
}
|
|
45
|
+
evidence: failed.map((result) => result.logPath),
|
|
46
|
+
},
|
|
47
47
|
};
|
|
48
48
|
}
|
package/dist/src/detect/index.js
CHANGED
|
@@ -26,7 +26,7 @@ async function buildDriftReport(input) {
|
|
|
26
26
|
baseSha: input.baseSha,
|
|
27
27
|
headSha: input.headSha,
|
|
28
28
|
trigger: input.trigger,
|
|
29
|
-
timestamp: new Date().toISOString()
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
30
|
};
|
|
31
31
|
const evidenceRoot = node_path_1.default.resolve(".docdrift", "evidence", runInfo.runId);
|
|
32
32
|
(0, fs_1.ensureDir)(evidenceRoot);
|
|
@@ -69,7 +69,7 @@ async function buildDriftReport(input) {
|
|
|
69
69
|
signals,
|
|
70
70
|
impactedDocs: [...impactedDocs],
|
|
71
71
|
recommendedAction: defaultRecommendation(docArea.mode, signals),
|
|
72
|
-
summary: summaries.filter(Boolean).join(" | ")
|
|
72
|
+
summary: summaries.filter(Boolean).join(" | "),
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
const report = {
|
|
@@ -78,15 +78,15 @@ async function buildDriftReport(input) {
|
|
|
78
78
|
baseSha: input.baseSha,
|
|
79
79
|
headSha: input.headSha,
|
|
80
80
|
trigger: input.trigger,
|
|
81
|
-
timestamp: runInfo.timestamp
|
|
81
|
+
timestamp: runInfo.timestamp,
|
|
82
82
|
},
|
|
83
|
-
items
|
|
83
|
+
items,
|
|
84
84
|
};
|
|
85
85
|
(0, fs_1.writeJsonFile)(node_path_1.default.resolve(".docdrift", "drift_report.json"), report);
|
|
86
86
|
(0, fs_1.writeJsonFile)(node_path_1.default.join(evidenceRoot, "changeset.json"), {
|
|
87
87
|
changedPaths,
|
|
88
88
|
diffSummary,
|
|
89
|
-
commits
|
|
89
|
+
commits,
|
|
90
90
|
});
|
|
91
91
|
return { report, changedPaths, evidenceRoot, runInfo, checkSummaries };
|
|
92
92
|
}
|
|
@@ -56,7 +56,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
56
56
|
"\n--- stdout ---",
|
|
57
57
|
exportResult.stdout,
|
|
58
58
|
"\n--- stderr ---",
|
|
59
|
-
exportResult.stderr
|
|
59
|
+
exportResult.stderr,
|
|
60
60
|
].join("\n"), "utf8");
|
|
61
61
|
if (exportResult.exitCode !== 0) {
|
|
62
62
|
return {
|
|
@@ -67,8 +67,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
67
67
|
kind: "weak_evidence",
|
|
68
68
|
tier: 2,
|
|
69
69
|
confidence: 0.35,
|
|
70
|
-
evidence: [exportLogPath]
|
|
71
|
-
}
|
|
70
|
+
evidence: [exportLogPath],
|
|
71
|
+
},
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
if (!node_fs_1.default.existsSync(openapi.generatedPath) || !node_fs_1.default.existsSync(openapi.publishedPath)) {
|
|
@@ -80,8 +80,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
80
80
|
kind: "weak_evidence",
|
|
81
81
|
tier: 2,
|
|
82
82
|
confidence: 0.35,
|
|
83
|
-
evidence: [exportLogPath]
|
|
84
|
-
}
|
|
83
|
+
evidence: [exportLogPath],
|
|
84
|
+
},
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
const generatedRaw = node_fs_1.default.readFileSync(openapi.generatedPath, "utf8");
|
|
@@ -94,7 +94,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
94
94
|
return {
|
|
95
95
|
impactedDocs: [openapi.publishedPath],
|
|
96
96
|
evidenceFiles: [exportLogPath],
|
|
97
|
-
summary: "No OpenAPI drift detected"
|
|
97
|
+
summary: "No OpenAPI drift detected",
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
const summary = summarizeSpecDelta(publishedJson, generatedJson);
|
|
@@ -107,7 +107,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
107
107
|
normalizedPublished,
|
|
108
108
|
"",
|
|
109
109
|
"# Generated (normalized)",
|
|
110
|
-
normalizedGenerated
|
|
110
|
+
normalizedGenerated,
|
|
111
111
|
].join("\n"), "utf8");
|
|
112
112
|
return {
|
|
113
113
|
impactedDocs: [...new Set([openapi.publishedPath, ...(docArea.patch.targets ?? [])])],
|
|
@@ -117,7 +117,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
|
|
|
117
117
|
kind: "openapi_diff",
|
|
118
118
|
tier: 1,
|
|
119
119
|
confidence: 0.95,
|
|
120
|
-
evidence: [diffPath]
|
|
121
|
-
}
|
|
120
|
+
evidence: [diffPath],
|
|
121
|
+
},
|
|
122
122
|
};
|
|
123
123
|
}
|
|
@@ -6,7 +6,7 @@ function attachmentBlock(attachmentUrls) {
|
|
|
6
6
|
return attachmentUrls.map((url, index) => `- ATTACHMENT ${index + 1}: ${url}`).join("\n");
|
|
7
7
|
}
|
|
8
8
|
function buildAutogenPrompt(input) {
|
|
9
|
-
|
|
9
|
+
const base = [
|
|
10
10
|
"You are Devin. Task: update API reference docs to match actual code/spec changes.",
|
|
11
11
|
"",
|
|
12
12
|
"EVIDENCE (attachments):",
|
|
@@ -30,12 +30,16 @@ function buildAutogenPrompt(input) {
|
|
|
30
30
|
" e) get blocked (status=BLOCKED + questions),",
|
|
31
31
|
" f) complete (status=DONE).",
|
|
32
32
|
"",
|
|
33
|
-
`Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence
|
|
33
|
+
`Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence.`,
|
|
34
34
|
].join("\n");
|
|
35
|
+
if (input.customAppend) {
|
|
36
|
+
return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
|
|
37
|
+
}
|
|
38
|
+
return base;
|
|
35
39
|
}
|
|
36
40
|
function buildConceptualPrompt(input) {
|
|
37
|
-
|
|
38
|
-
"You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes.",
|
|
41
|
+
const base = [
|
|
42
|
+
"You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes. i..e GUIDES",
|
|
39
43
|
"",
|
|
40
44
|
"EVIDENCE (attachments):",
|
|
41
45
|
attachmentBlock(input.attachmentUrls),
|
|
@@ -50,6 +54,10 @@ function buildConceptualPrompt(input) {
|
|
|
50
54
|
"- Update at planning, editing, verifying, open-pr, blocked, done milestones.",
|
|
51
55
|
"- If blocked, fill blocked.questions with concrete, reviewer-actionable questions.",
|
|
52
56
|
"",
|
|
53
|
-
"Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text."
|
|
57
|
+
"Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text.",
|
|
54
58
|
].join("\n");
|
|
59
|
+
if (input.customAppend) {
|
|
60
|
+
return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
|
|
61
|
+
}
|
|
62
|
+
return base;
|
|
55
63
|
}
|
|
@@ -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
|
};
|
package/dist/src/devin/v1.js
CHANGED
|
@@ -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) {
|
|
@@ -19,7 +19,7 @@ async function postCommitComment(input) {
|
|
|
19
19
|
owner,
|
|
20
20
|
repo,
|
|
21
21
|
commit_sha: input.commitSha,
|
|
22
|
-
body: input.body
|
|
22
|
+
body: input.body,
|
|
23
23
|
});
|
|
24
24
|
return response.data.html_url;
|
|
25
25
|
}
|
|
@@ -31,7 +31,7 @@ async function createIssue(input) {
|
|
|
31
31
|
repo,
|
|
32
32
|
title: input.issue.title,
|
|
33
33
|
body: input.issue.body,
|
|
34
|
-
labels: input.issue.labels
|
|
34
|
+
labels: input.issue.labels,
|
|
35
35
|
});
|
|
36
36
|
return response.data.html_url;
|
|
37
37
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -45,7 +45,7 @@ function inferQuestions(structured) {
|
|
|
45
45
|
}
|
|
46
46
|
return [
|
|
47
47
|
"Which conceptual docs should be updated for this behavior change?",
|
|
48
|
-
"What are the exact user-visible semantics after this merge?"
|
|
48
|
+
"What are the exact user-visible semantics after this merge?",
|
|
49
49
|
];
|
|
50
50
|
}
|
|
51
51
|
async function executeSession(input) {
|
|
@@ -60,14 +60,16 @@ async function executeSession(input) {
|
|
|
60
60
|
attachmentUrls,
|
|
61
61
|
verificationCommands: input.config.policy.verification.commands,
|
|
62
62
|
allowlist: input.config.policy.allowlist,
|
|
63
|
-
confidenceThreshold: input.config.policy.confidence.autopatchThreshold
|
|
63
|
+
confidenceThreshold: input.config.policy.confidence.autopatchThreshold,
|
|
64
|
+
customAppend: input.config.devin.customInstructionContent ?? undefined,
|
|
64
65
|
})
|
|
65
66
|
: (0, prompts_1.buildConceptualPrompt)({
|
|
66
67
|
item: input.item,
|
|
67
68
|
attachmentUrls,
|
|
68
69
|
verificationCommands: input.config.policy.verification.commands,
|
|
69
70
|
allowlist: input.config.policy.allowlist,
|
|
70
|
-
confidenceThreshold: input.config.policy.confidence.autopatchThreshold
|
|
71
|
+
confidenceThreshold: input.config.policy.confidence.autopatchThreshold,
|
|
72
|
+
customAppend: input.config.devin.customInstructionContent ?? undefined,
|
|
71
73
|
});
|
|
72
74
|
const session = await (0, v1_1.devinCreateSession)(input.apiKey, {
|
|
73
75
|
prompt,
|
|
@@ -76,13 +78,13 @@ async function executeSession(input) {
|
|
|
76
78
|
tags: [...new Set([...(input.config.devin.tags ?? []), "docdrift", input.item.docArea])],
|
|
77
79
|
attachments: attachmentUrls,
|
|
78
80
|
structured_output: {
|
|
79
|
-
schema: schemas_1.PatchPlanSchema
|
|
81
|
+
schema: schemas_1.PatchPlanSchema,
|
|
80
82
|
},
|
|
81
83
|
metadata: {
|
|
82
84
|
repository: input.repository,
|
|
83
85
|
docArea: input.item.docArea,
|
|
84
|
-
mode: input.item.mode
|
|
85
|
-
}
|
|
86
|
+
mode: input.item.mode,
|
|
87
|
+
},
|
|
86
88
|
});
|
|
87
89
|
const finalSession = await (0, v1_1.pollUntilTerminal)(input.apiKey, session.session_id);
|
|
88
90
|
const structured = parseStructured(finalSession);
|
|
@@ -96,7 +98,7 @@ async function executeSession(input) {
|
|
|
96
98
|
: verificationCommands.map(() => "not reported");
|
|
97
99
|
const verification = verificationCommands.map((command, idx) => ({
|
|
98
100
|
command,
|
|
99
|
-
result: verificationResults[idx] ?? "not reported"
|
|
101
|
+
result: verificationResults[idx] ?? "not reported",
|
|
100
102
|
}));
|
|
101
103
|
if (prUrl) {
|
|
102
104
|
return {
|
|
@@ -104,7 +106,7 @@ async function executeSession(input) {
|
|
|
104
106
|
summary: String(structured?.summary ?? "PR opened by Devin"),
|
|
105
107
|
sessionUrl: session.url,
|
|
106
108
|
prUrl,
|
|
107
|
-
verification
|
|
109
|
+
verification,
|
|
108
110
|
};
|
|
109
111
|
}
|
|
110
112
|
if (status === "blocked" || structured?.status === "BLOCKED") {
|
|
@@ -113,14 +115,14 @@ async function executeSession(input) {
|
|
|
113
115
|
summary: String(structured?.blocked?.reason ?? structured?.summary ?? "Session blocked"),
|
|
114
116
|
sessionUrl: session.url,
|
|
115
117
|
questions: inferQuestions(structured),
|
|
116
|
-
verification
|
|
118
|
+
verification,
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
return {
|
|
120
122
|
outcome: "NO_CHANGE",
|
|
121
123
|
summary: String(structured?.summary ?? "Session completed without PR"),
|
|
122
124
|
sessionUrl: session.url,
|
|
123
|
-
verification
|
|
125
|
+
verification,
|
|
124
126
|
};
|
|
125
127
|
}
|
|
126
128
|
async function runDetect(options) {
|
|
@@ -135,7 +137,7 @@ async function runDetect(options) {
|
|
|
135
137
|
repo,
|
|
136
138
|
baseSha: options.baseSha,
|
|
137
139
|
headSha: options.headSha,
|
|
138
|
-
trigger: options.trigger ?? "manual"
|
|
140
|
+
trigger: options.trigger ?? "manual",
|
|
139
141
|
});
|
|
140
142
|
(0, log_1.logInfo)(`Drift items detected: ${report.items.length}`);
|
|
141
143
|
return { hasDrift: report.items.length > 0 };
|
|
@@ -155,7 +157,7 @@ async function runDocDrift(options) {
|
|
|
155
157
|
repo,
|
|
156
158
|
baseSha: options.baseSha,
|
|
157
159
|
headSha: options.headSha,
|
|
158
|
-
trigger: options.trigger ?? "manual"
|
|
160
|
+
trigger: options.trigger ?? "manual",
|
|
159
161
|
});
|
|
160
162
|
const docAreaByName = new Map(config.docAreas.map((area) => [area.name, area]));
|
|
161
163
|
let state = (0, state_1.loadState)();
|
|
@@ -168,7 +170,7 @@ async function runDocDrift(options) {
|
|
|
168
170
|
blockedCount: 0,
|
|
169
171
|
timeToSessionTerminalMs: [],
|
|
170
172
|
docAreaCounts: {},
|
|
171
|
-
noiseRateProxy: 0
|
|
173
|
+
noiseRateProxy: 0,
|
|
172
174
|
};
|
|
173
175
|
for (const item of report.items) {
|
|
174
176
|
metrics.docAreaCounts[item.docArea] = (metrics.docAreaCounts[item.docArea] ?? 0) + 1;
|
|
@@ -183,14 +185,14 @@ async function runDocDrift(options) {
|
|
|
183
185
|
state,
|
|
184
186
|
repo,
|
|
185
187
|
baseSha: options.baseSha,
|
|
186
|
-
headSha: options.headSha
|
|
188
|
+
headSha: options.headSha,
|
|
187
189
|
});
|
|
188
190
|
if (decision.action === "NOOP") {
|
|
189
191
|
results.push({
|
|
190
192
|
docArea: item.docArea,
|
|
191
193
|
decision,
|
|
192
194
|
outcome: "NO_CHANGE",
|
|
193
|
-
summary: decision.reason
|
|
195
|
+
summary: decision.reason,
|
|
194
196
|
});
|
|
195
197
|
continue;
|
|
196
198
|
}
|
|
@@ -205,14 +207,14 @@ async function runDocDrift(options) {
|
|
|
205
207
|
decision,
|
|
206
208
|
outcome,
|
|
207
209
|
summary,
|
|
208
|
-
prUrl: existingPr
|
|
210
|
+
prUrl: existingPr,
|
|
209
211
|
});
|
|
210
212
|
state = (0, engine_1.applyDecisionToState)({
|
|
211
213
|
state,
|
|
212
214
|
decision,
|
|
213
215
|
docArea: item.docArea,
|
|
214
216
|
outcome,
|
|
215
|
-
link: existingPr
|
|
217
|
+
link: existingPr,
|
|
216
218
|
});
|
|
217
219
|
continue;
|
|
218
220
|
}
|
|
@@ -221,7 +223,10 @@ async function runDocDrift(options) {
|
|
|
221
223
|
let sessionOutcome = {
|
|
222
224
|
outcome: "NO_CHANGE",
|
|
223
225
|
summary: "Skipped Devin session",
|
|
224
|
-
verification: config.policy.verification.commands.map((command) => ({
|
|
226
|
+
verification: config.policy.verification.commands.map((command) => ({
|
|
227
|
+
command,
|
|
228
|
+
result: "not run",
|
|
229
|
+
})),
|
|
225
230
|
};
|
|
226
231
|
if (devinApiKey) {
|
|
227
232
|
const sessionStart = Date.now();
|
|
@@ -230,7 +235,7 @@ async function runDocDrift(options) {
|
|
|
230
235
|
repository: repo,
|
|
231
236
|
item,
|
|
232
237
|
attachmentPaths,
|
|
233
|
-
config
|
|
238
|
+
config,
|
|
234
239
|
});
|
|
235
240
|
metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
|
|
236
241
|
}
|
|
@@ -240,12 +245,17 @@ async function runDocDrift(options) {
|
|
|
240
245
|
outcome: "BLOCKED",
|
|
241
246
|
summary: "DEVIN_API_KEY missing; cannot start Devin session",
|
|
242
247
|
questions: ["Set DEVIN_API_KEY in environment or GitHub Actions secrets"],
|
|
243
|
-
verification: config.policy.verification.commands.map((command) => ({
|
|
248
|
+
verification: config.policy.verification.commands.map((command) => ({
|
|
249
|
+
command,
|
|
250
|
+
result: "not run",
|
|
251
|
+
})),
|
|
244
252
|
};
|
|
245
253
|
}
|
|
246
254
|
let issueUrl;
|
|
247
255
|
if (githubToken &&
|
|
248
|
-
(decision.action === "OPEN_ISSUE" ||
|
|
256
|
+
(decision.action === "OPEN_ISSUE" ||
|
|
257
|
+
sessionOutcome.outcome === "BLOCKED" ||
|
|
258
|
+
sessionOutcome.outcome === "NO_CHANGE")) {
|
|
249
259
|
issueUrl = await (0, client_1.createIssue)({
|
|
250
260
|
token: githubToken,
|
|
251
261
|
repository: repo,
|
|
@@ -254,11 +264,13 @@ async function runDocDrift(options) {
|
|
|
254
264
|
body: (0, client_1.renderBlockedIssueBody)({
|
|
255
265
|
docArea: item.docArea,
|
|
256
266
|
evidenceSummary: item.summary,
|
|
257
|
-
questions: sessionOutcome.questions ?? [
|
|
258
|
-
|
|
267
|
+
questions: sessionOutcome.questions ?? [
|
|
268
|
+
"Please confirm intended behavior and doc wording.",
|
|
269
|
+
],
|
|
270
|
+
sessionUrl: sessionOutcome.sessionUrl,
|
|
259
271
|
}),
|
|
260
|
-
labels: ["docdrift"]
|
|
261
|
-
}
|
|
272
|
+
labels: ["docdrift"],
|
|
273
|
+
},
|
|
262
274
|
});
|
|
263
275
|
metrics.issuesOpened += 1;
|
|
264
276
|
sessionOutcome.outcome = "ISSUE_OPENED";
|
|
@@ -276,7 +288,7 @@ async function runDocDrift(options) {
|
|
|
276
288
|
summary: sessionOutcome.summary,
|
|
277
289
|
sessionUrl: sessionOutcome.sessionUrl,
|
|
278
290
|
prUrl: sessionOutcome.prUrl,
|
|
279
|
-
issueUrl
|
|
291
|
+
issueUrl,
|
|
280
292
|
};
|
|
281
293
|
results.push(result);
|
|
282
294
|
if (githubToken) {
|
|
@@ -288,13 +300,13 @@ async function runDocDrift(options) {
|
|
|
288
300
|
sessionUrl: sessionOutcome.sessionUrl,
|
|
289
301
|
prUrl: sessionOutcome.prUrl,
|
|
290
302
|
issueUrl,
|
|
291
|
-
validation: sessionOutcome.verification
|
|
303
|
+
validation: sessionOutcome.verification,
|
|
292
304
|
});
|
|
293
305
|
await (0, client_1.postCommitComment)({
|
|
294
306
|
token: githubToken,
|
|
295
307
|
repository: repo,
|
|
296
308
|
commitSha,
|
|
297
|
-
body
|
|
309
|
+
body,
|
|
298
310
|
});
|
|
299
311
|
}
|
|
300
312
|
state = (0, engine_1.applyDecisionToState)({
|
|
@@ -302,16 +314,18 @@ async function runDocDrift(options) {
|
|
|
302
314
|
decision,
|
|
303
315
|
docArea: item.docArea,
|
|
304
316
|
outcome: sessionOutcome.outcome,
|
|
305
|
-
link: sessionOutcome.prUrl ?? issueUrl
|
|
317
|
+
link: sessionOutcome.prUrl ?? issueUrl,
|
|
306
318
|
});
|
|
307
319
|
}
|
|
308
320
|
(0, state_1.saveState)(state);
|
|
309
321
|
metrics.noiseRateProxy =
|
|
310
|
-
metrics.driftItemsDetected === 0
|
|
322
|
+
metrics.driftItemsDetected === 0
|
|
323
|
+
? 0
|
|
324
|
+
: Number((metrics.prsOpened / metrics.driftItemsDetected).toFixed(4));
|
|
311
325
|
(0, bundle_1.writeMetrics)(metrics);
|
|
312
326
|
(0, log_1.logInfo)("Run complete", {
|
|
313
327
|
items: report.items.length,
|
|
314
|
-
elapsedMs: Date.now() - startedAt
|
|
328
|
+
elapsedMs: Date.now() - startedAt,
|
|
315
329
|
});
|
|
316
330
|
return results;
|
|
317
331
|
}
|
package/dist/src/model/state.js
CHANGED
|
@@ -70,21 +70,21 @@ function decidePolicy(input) {
|
|
|
70
70
|
docArea: item.docArea,
|
|
71
71
|
baseSha: input.baseSha,
|
|
72
72
|
headSha: input.headSha,
|
|
73
|
-
action
|
|
73
|
+
action,
|
|
74
74
|
});
|
|
75
75
|
if (state.idempotency[idempotencyKey]) {
|
|
76
76
|
return {
|
|
77
77
|
action: "NOOP",
|
|
78
78
|
confidence,
|
|
79
79
|
reason: "Idempotency key already processed",
|
|
80
|
-
idempotencyKey
|
|
80
|
+
idempotencyKey,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
return {
|
|
84
84
|
action,
|
|
85
85
|
confidence,
|
|
86
86
|
reason,
|
|
87
|
-
idempotencyKey
|
|
87
|
+
idempotencyKey,
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
function applyDecisionToState(input) {
|
|
@@ -94,7 +94,7 @@ function applyDecisionToState(input) {
|
|
|
94
94
|
createdAt: new Date().toISOString(),
|
|
95
95
|
action: input.decision.action,
|
|
96
96
|
outcome: input.outcome,
|
|
97
|
-
link: input.link
|
|
97
|
+
link: input.link,
|
|
98
98
|
};
|
|
99
99
|
next.idempotency[input.decision.idempotencyKey] = record;
|
|
100
100
|
if (input.outcome === "PR_OPENED") {
|
package/dist/src/utils/exec.js
CHANGED
|
@@ -8,7 +8,7 @@ async function execCommand(command, cwd = process.cwd()) {
|
|
|
8
8
|
try {
|
|
9
9
|
const { stdout, stderr } = await exec(command, {
|
|
10
10
|
cwd,
|
|
11
|
-
maxBuffer: 10 * 1024 * 1024
|
|
11
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
12
12
|
});
|
|
13
13
|
return { command, stdout, stderr, exitCode: 0 };
|
|
14
14
|
}
|
|
@@ -18,7 +18,7 @@ async function execCommand(command, cwd = process.cwd()) {
|
|
|
18
18
|
command,
|
|
19
19
|
stdout: e.stdout ?? "",
|
|
20
20
|
stderr: e.stderr ?? String(error),
|
|
21
|
-
exitCode: typeof e.code === "number" ? e.code : 1
|
|
21
|
+
exitCode: typeof e.code === "number" ? e.code : 1,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devinnn/docdrift",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Detect and remediate documentation drift with Devin sessions",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -11,9 +11,20 @@
|
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=20"
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/src"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": ""
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"docs",
|
|
23
|
+
"drift",
|
|
24
|
+
"openapi",
|
|
25
|
+
"devin",
|
|
26
|
+
"github-actions"
|
|
27
|
+
],
|
|
17
28
|
"license": "MIT",
|
|
18
29
|
"scripts": {
|
|
19
30
|
"build": "tsc -p tsconfig.json",
|
|
@@ -21,7 +32,9 @@
|
|
|
21
32
|
"lint": "eslint .",
|
|
22
33
|
"format": "prettier -w .",
|
|
23
34
|
"openapi:export": "tsx apps/api/scripts/export-openapi.ts",
|
|
24
|
-
"docs:
|
|
35
|
+
"docs:gen": "npm run --prefix apps/docs-site docusaurus -- gen-api-docs api",
|
|
36
|
+
"docs:build": "npm run --prefix apps/docs-site build",
|
|
37
|
+
"docs:serve": "npm run --prefix apps/docs-site start",
|
|
25
38
|
"docdrift": "tsx src/cli.ts"
|
|
26
39
|
},
|
|
27
40
|
"dependencies": {
|