@prabhat-singh/docdrift 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +174 -2
- package/bin/docdrift.js +232 -0
- package/package.json +42 -4
- package/src/banner.js +60 -0
- package/src/check.js +228 -0
- package/src/config.js +75 -0
- package/src/docs.js +99 -0
- package/src/fix.js +299 -0
- package/src/llm.js +42 -0
- package/src/match.js +73 -0
- package/src/scan.js +343 -0
- package/src/style.js +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Prabhat Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,175 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DocDrift
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Find stale and undocumented REST endpoints by comparing your Spring Boot code with your docs.
|
|
4
|
+
|
|
5
|
+
## Why DocDrift?
|
|
6
|
+
|
|
7
|
+
Docs drift from Spring Boot controllers silently: routes get renamed, removed or added without a README update.
|
|
8
|
+
DocDrift finds stale and undocumented endpoints deterministically, with no AI in the detection path.
|
|
9
|
+
AI is used only to draft the fix, and a code-based verifier checks that draft before it is marked VERIFIED.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd my-spring-app
|
|
15
|
+
npx github:prabhatsingh415/docdrift check
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That scans every `.java` file under the current folder and compares it with `README.md` and `docs/*.md` in that folder.
|
|
19
|
+
|
|
20
|
+
If the docs live elsewhere:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
docdrift check ./my-spring-app/src/main/java ./path/to/docs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What it does
|
|
27
|
+
|
|
28
|
+
- **STALE** - an endpoint is documented but not present in the code.
|
|
29
|
+
- **UNDOCUMENTED** - an endpoint exists in the code but is not documented.
|
|
30
|
+
|
|
31
|
+
Both come straight from the code and the docs, with no AI involved.
|
|
32
|
+
|
|
33
|
+
AI is optional and only used to draft a README fix. A code-based verifier checks the draft: it must mention only endpoints that exist in the code, remove stale ones and add undocumented ones. Until that check passes, the draft is **UNVERIFIED**. The original README is never overwritten - the draft is written to `README.drift.md`.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
For a one-off run without installing:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @prabhat-singh/docdrift check .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For a global install:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g @prabhat-singh/docdrift
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
After a global install, use `docdrift ...` directly. Requires Node 18+. Zero dependencies.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
docdrift scan
|
|
55
|
+
docdrift docs
|
|
56
|
+
docdrift check
|
|
57
|
+
docdrift fix
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run these from your project root. All four commands also accept optional paths: `scan [dir]`, `docs [dir]`, `check [codeDir] [docsDir]`, and `fix [codeDir] [docsDir]`.
|
|
61
|
+
|
|
62
|
+
- `scan [dir]` - print the endpoints found in `.java` files as JSON.
|
|
63
|
+
- `docs [dir]` - print the endpoints documented in `README.md` and `docs/*.md` as JSON.
|
|
64
|
+
- `check [codeDir] [docsDir]` - compare code and docs and print a report. `docsDir` defaults to `codeDir`.
|
|
65
|
+
- `fix [codeDir] [docsDir]` - ask a model to draft a fixed README, verify the draft, and write it to `README.drift.md`.
|
|
66
|
+
|
|
67
|
+
Flags:
|
|
68
|
+
|
|
69
|
+
- `--dry-run` - print the writer prompt for `fix` without calling any model.
|
|
70
|
+
- `--strict` - report Spring framework endpoints as STALE instead of ignoring them.
|
|
71
|
+
- `--no-color` - disable ANSI colors.
|
|
72
|
+
|
|
73
|
+
Exit codes:
|
|
74
|
+
|
|
75
|
+
- `check`: `1` when drift is found, `0` otherwise.
|
|
76
|
+
- `fix`: `0` for VERIFIED or NO_DRIFT, `1` for UNVERIFIED, `2` for a config error.
|
|
77
|
+
|
|
78
|
+
## Example output
|
|
79
|
+
|
|
80
|
+
Real output from `docdrift check` on a private Spring Boot project whose README had seeded drift:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
docdrift check: 23 code endpoints, 16 documented endpoints, 14 matched
|
|
84
|
+
|
|
85
|
+
STALE (documented, not in code)
|
|
86
|
+
GET /giveaway/winners (Readme.md:7)
|
|
87
|
+
DELETE /user/delete (Readme.md:31)
|
|
88
|
+
|
|
89
|
+
UNDOCUMENTED (in code, not documented)
|
|
90
|
+
POST /api/webhooks/razorpay (com/LuckyHub/Backend/controller/WebhookController.java:19)
|
|
91
|
+
PUT /user/changePassword (com/LuckyHub/Backend/controller/UserController.java:181)
|
|
92
|
+
DELETE /user/confirm-delete (com/LuckyHub/Backend/controller/UserController.java:212)
|
|
93
|
+
POST /user/logout (com/LuckyHub/Backend/controller/UserController.java:193)
|
|
94
|
+
GET /user/me (com/LuckyHub/Backend/controller/UserController.java:146)
|
|
95
|
+
POST /user/request-delete (com/LuckyHub/Backend/controller/UserController.java:204)
|
|
96
|
+
GET /user/resendToken (com/LuckyHub/Backend/controller/UserController.java:107)
|
|
97
|
+
PUT /user/updateAvatar (com/LuckyHub/Backend/controller/UserController.java:168)
|
|
98
|
+
PUT /user/updateName (com/LuckyHub/Backend/controller/UserController.java:156)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
From a checkout of this repo:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
node bin/docdrift.js check test/fixtures-check --no-color
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
docdrift check: 3 code endpoints, 3 documented endpoints, 2 matched
|
|
109
|
+
|
|
110
|
+
STALE (documented, not in code)
|
|
111
|
+
GET /api/legacy (README.md:7)
|
|
112
|
+
|
|
113
|
+
UNDOCUMENTED (in code, not documented)
|
|
114
|
+
DELETE /api/users/{id} (UserController.java:15)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Example run on a demo project
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
Status: VERIFIED
|
|
121
|
+
Model: recursive/flint-v1.0:free
|
|
122
|
+
Attempts: 1
|
|
123
|
+
Stale removed: 2 | Undocumented added: 9
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuring `fix`
|
|
127
|
+
|
|
128
|
+
`fix` works with any OpenAI-compatible endpoint; bring your own key. Set these variables as environment variables or in a `.env` file:
|
|
129
|
+
|
|
130
|
+
- `DOCDRIFT_API_KEY`
|
|
131
|
+
- `DOCDRIFT_BASE_URL`
|
|
132
|
+
- `DOCDRIFT_MODEL`
|
|
133
|
+
|
|
134
|
+
Never commit `.env` (a `.env.example` is included). The key is optional for local endpoints such as Ollama (`http://localhost:11434/v1`).
|
|
135
|
+
|
|
136
|
+
`fix --dry-run` prints the prompt without calling any model. `check`, `scan` and `docs` need no key and no AI.
|
|
137
|
+
|
|
138
|
+
This project was demoed with the open-weight model `recursive/flint-v1.0:free` through OpenRouter.
|
|
139
|
+
|
|
140
|
+
## How it works
|
|
141
|
+
|
|
142
|
+
- **Scanner** - reads `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping` and `@RequestMapping` from `.java` files, joins the class-level prefix, ignores comments, and skips non-literal paths with a warning.
|
|
143
|
+
- **Docs parser** - finds `"METHOD /path"` in prose, inline code, code blocks and tables.
|
|
144
|
+
- **Matcher** - treats path parameters `{id}`, `:id` and `{userId}` as equal, ignores trailing slashes and query strings, and lets a code `@RequestMapping` without a method match any documented method. Spring library endpoints such as `/actuator` and `/swagger-ui` are reported as ignored instead of stale unless `--strict` is used.
|
|
145
|
+
- **Verifier loop** - the writer gets up to 2 retries to produce a draft that passes the code-based checks.
|
|
146
|
+
|
|
147
|
+
## Tests and evaluation
|
|
148
|
+
|
|
149
|
+
- **60 unit tests** with `node:test` (`npm test`).
|
|
150
|
+
- **Seeded-drift evaluation** (`npm run eval`): for 5 Java repos (4 public repos and one private project), 150 trials. Endpoints were removed from and fake endpoints added to a README generated from the code scan, plus harmless noise (renamed path parameters, trailing slashes). Result: **100% precision and recall** for both STALE and UNDOCUMENTED, with no failures. This is a controlled test: the README is generated from the scan, so it validates the matcher and the docs parser, not the scanner and not messy real-world READMEs. Results are in `evals/RESULTS.md`.
|
|
151
|
+
- **Real READMEs**: `check` was run on 4 public Spring Boot repos with their own READMEs. DocDrift reported no drift on 3 of them. The one false alarm (documented `/actuator/health` and `/swagger-ui.html`, provided by libraries) led to the framework-endpoint rule. No ground truth exists for real READMEs, so no recall is claimed.
|
|
152
|
+
- **Scanner**: on 5 repos it found 51 endpoints. In 4 repos the count matched a grep count; in the 5th, grep also counted a commented-out `@GetMapping` that the scanner correctly skipped (checked by hand).
|
|
153
|
+
- **`fix`**: verified on the demo project above. A systematic success rate for `fix` has not been measured yet because the free model endpoint was rate-limited.
|
|
154
|
+
|
|
155
|
+
## Limitations
|
|
156
|
+
|
|
157
|
+
- Only Spring Boot REST annotations in `.java` files (no Kotlin, no Express, no OpenAPI files).
|
|
158
|
+
- Constants in paths (like `BASE + "/x"`) are skipped with a warning, not resolved.
|
|
159
|
+
- `server.servlet.context-path` and class-level inheritance are not handled.
|
|
160
|
+
- A `@RequestMapping` without a method matches any documented method, so it can hide a wrong HTTP method.
|
|
161
|
+
- Docs are matched textually as `"METHOD /path"`, so endpoints written in other styles (for example a bare path without the method) are not found.
|
|
162
|
+
- The verifier only checks endpoints, not the rest of the text a model writes.
|
|
163
|
+
- The free open model used for the demo can be rate-limited.
|
|
164
|
+
|
|
165
|
+
## Built with Alan AI
|
|
166
|
+
|
|
167
|
+
This project was built with Alan AI (the code was written by Alan and reviewed and tested by the author).
|
|
168
|
+
|
|
169
|
+
## Not related
|
|
170
|
+
|
|
171
|
+
Not related to the npm package `docdrift` by Nico Schwandner, which links business decisions to code.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT.
|
package/bin/docdrift.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { scanJava } from '../src/scan.js';
|
|
6
|
+
import { scanDocs, extractFromText } from '../src/docs.js';
|
|
7
|
+
import { check } from '../src/check.js';
|
|
8
|
+
import { compare } from '../src/match.js';
|
|
9
|
+
import { loadConfig } from '../src/config.js';
|
|
10
|
+
import { chat } from '../src/llm.js';
|
|
11
|
+
import {
|
|
12
|
+
buildWriterMessages,
|
|
13
|
+
fixDocs,
|
|
14
|
+
formatFixReport,
|
|
15
|
+
writeDriftFile,
|
|
16
|
+
} from '../src/fix.js';
|
|
17
|
+
import { createStyle, shouldUseColor, supportsTrueColor } from '../src/style.js';
|
|
18
|
+
import { compactBanner, fullBanner } from '../src/banner.js';
|
|
19
|
+
|
|
20
|
+
const { version } = JSON.parse(
|
|
21
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const rawArgs = process.argv.slice(2);
|
|
25
|
+
const style = createStyle(
|
|
26
|
+
shouldUseColor(process.stdout, process.env, rawArgs),
|
|
27
|
+
supportsTrueColor(process.env),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (rawArgs.includes('--help')) {
|
|
31
|
+
process.stdout.write(`${fullBanner(style, version)}\n\n${helpText()}\n`);
|
|
32
|
+
} else if (rawArgs.includes('--version')) {
|
|
33
|
+
process.stdout.write(`${fullBanner(style, version)}\n`);
|
|
34
|
+
} else {
|
|
35
|
+
const flags = new Set(['--dry-run', '--no-color', '--strict']);
|
|
36
|
+
const [command, target, docsTarget] = rawArgs.filter(
|
|
37
|
+
(arg) => !flags.has(arg),
|
|
38
|
+
);
|
|
39
|
+
const commands = { scan: scanJava, docs: scanDocs };
|
|
40
|
+
|
|
41
|
+
if (command === undefined) {
|
|
42
|
+
process.stdout.write(`${fullBanner(style, version)}\n\n${helpText()}\n`);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
} else if (command === 'fix') {
|
|
45
|
+
process.exitCode = await runFix(
|
|
46
|
+
target ?? '.',
|
|
47
|
+
docsTarget,
|
|
48
|
+
rawArgs.includes('--dry-run'),
|
|
49
|
+
style,
|
|
50
|
+
);
|
|
51
|
+
} else if (command === 'check') {
|
|
52
|
+
const codeDir = target ?? '.';
|
|
53
|
+
const docsDir = docsTarget ?? codeDir;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const codeEndpoints = scanJava(codeDir);
|
|
57
|
+
if (codeEndpoints.length === 0) {
|
|
58
|
+
console.error(noEndpointsMessage(codeDir));
|
|
59
|
+
process.exitCode = 2;
|
|
60
|
+
} else {
|
|
61
|
+
if (style.enabled) {
|
|
62
|
+
process.stdout.write(`${compactBanner(style, version)}\n\n`);
|
|
63
|
+
}
|
|
64
|
+
const { report, exitCode } = check(
|
|
65
|
+
codeDir,
|
|
66
|
+
docsDir,
|
|
67
|
+
style.enabled ? style : null,
|
|
68
|
+
{
|
|
69
|
+
strict: rawArgs.includes('--strict'),
|
|
70
|
+
codeEndpoints,
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
process.stdout.write(`${report}\n`);
|
|
74
|
+
process.exitCode = exitCode;
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`docdrift: ${error.message}`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
} else if (!Object.hasOwn(commands, command)) {
|
|
81
|
+
console.error('usage: docdrift <scan|docs|check|fix> [dir]');
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
} else {
|
|
84
|
+
try {
|
|
85
|
+
const endpoints = commands[command](target ?? '.');
|
|
86
|
+
process.stdout.write(`${JSON.stringify(endpoints, null, 2)}\n`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`docdrift: ${error.message}`);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runFix(codeDir, docsTarget, isDryRun, style) {
|
|
95
|
+
const docsDir = docsTarget ?? codeDir;
|
|
96
|
+
|
|
97
|
+
let codeEndpoints;
|
|
98
|
+
try {
|
|
99
|
+
codeEndpoints = scanJava(codeDir);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`docdrift: ${error.message}`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (codeEndpoints.length === 0) {
|
|
106
|
+
console.error(noEndpointsMessage(codeDir));
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (style.enabled) {
|
|
111
|
+
process.stdout.write(`${compactBanner(style, version)}\n\n`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let readmeName;
|
|
115
|
+
try {
|
|
116
|
+
readmeName = readdirSync(docsDir).find(
|
|
117
|
+
(name) => name.toLowerCase() === 'readme.md',
|
|
118
|
+
);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`docdrift: ${error.message}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!readmeName) {
|
|
125
|
+
console.error(`docdrift: no README.md found in ${docsDir}`);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const readmePath = join(docsDir, readmeName);
|
|
130
|
+
const readmeText = readFileSync(readmePath, 'utf8');
|
|
131
|
+
const docEndpoints = extractFromText(readmeText).map((endpoint) => ({
|
|
132
|
+
...endpoint,
|
|
133
|
+
file: readmePath,
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
if (isDryRun) {
|
|
137
|
+
const messages = buildWriterMessages({
|
|
138
|
+
codeEndpoints,
|
|
139
|
+
docEndpoints,
|
|
140
|
+
readmeText,
|
|
141
|
+
});
|
|
142
|
+
for (const message of messages) {
|
|
143
|
+
process.stdout.write(`[${message.role}]\n${message.content}\n\n`);
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const config = loadConfig();
|
|
149
|
+
if (config.error) {
|
|
150
|
+
console.error(`docdrift: ${config.error}`);
|
|
151
|
+
return 2;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = await fixDocs({
|
|
156
|
+
codeEndpoints,
|
|
157
|
+
docEndpoints,
|
|
158
|
+
readmeText,
|
|
159
|
+
chatFn: (messages) => chat(config, messages),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const { stale, undocumented } = compare(codeEndpoints, docEndpoints);
|
|
163
|
+
|
|
164
|
+
if (result.status === 'NO_DRIFT') {
|
|
165
|
+
process.stdout.write(
|
|
166
|
+
`${formatFixReport(
|
|
167
|
+
{
|
|
168
|
+
status: result.status,
|
|
169
|
+
model: config.model,
|
|
170
|
+
attempts: 0,
|
|
171
|
+
staleRemoved: 0,
|
|
172
|
+
undocumentedAdded: 0,
|
|
173
|
+
draftPath: null,
|
|
174
|
+
},
|
|
175
|
+
style.enabled ? style : null,
|
|
176
|
+
)}\n`,
|
|
177
|
+
);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const draftPath = writeDriftFile(readmePath, result.draft);
|
|
182
|
+
process.stdout.write(
|
|
183
|
+
`${formatFixReport(
|
|
184
|
+
{
|
|
185
|
+
status: result.status,
|
|
186
|
+
model: config.model,
|
|
187
|
+
attempts: result.attempts,
|
|
188
|
+
staleRemoved: stale.length - result.leftover.length,
|
|
189
|
+
undocumentedAdded: undocumented.length - result.missing.length,
|
|
190
|
+
draftPath,
|
|
191
|
+
invented: result.invented,
|
|
192
|
+
missing: result.missing,
|
|
193
|
+
leftover: result.leftover,
|
|
194
|
+
},
|
|
195
|
+
style.enabled ? style : null,
|
|
196
|
+
)}\n`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return result.status === 'VERIFIED' ? 0 : 1;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error(`docdrift: ${error.message}`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function noEndpointsMessage(codeDir) {
|
|
207
|
+
const label =
|
|
208
|
+
codeDir === '.' ? basename(process.cwd()) || codeDir : codeDir;
|
|
209
|
+
return `No Spring REST endpoints found in "${label}" - run docdrift from your project root or pass the path: docdrift check <codeDir> [docsDir]`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function helpText() {
|
|
213
|
+
return [
|
|
214
|
+
'Usage: docdrift <command> [paths] [options]',
|
|
215
|
+
'',
|
|
216
|
+
'Commands:',
|
|
217
|
+
' scan [dir] Print code endpoints as JSON',
|
|
218
|
+
' docs [dir] Print documented endpoints as JSON',
|
|
219
|
+
' check [codeDir] [docsDir] Compare code against docs',
|
|
220
|
+
' fix [codeDir] [docsDir] Draft a verified README fix',
|
|
221
|
+
'',
|
|
222
|
+
'Options:',
|
|
223
|
+
' --strict Report Spring framework endpoints as STALE',
|
|
224
|
+
' --dry-run Print the writer prompt, no API call',
|
|
225
|
+
' --no-color Disable ANSI colors',
|
|
226
|
+
'',
|
|
227
|
+
'Environment for fix:',
|
|
228
|
+
' DOCDRIFT_API_KEY',
|
|
229
|
+
' DOCDRIFT_BASE_URL',
|
|
230
|
+
' DOCDRIFT_MODEL',
|
|
231
|
+
].join('\n');
|
|
232
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prabhat-singh/docdrift",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Find stale and undocumented REST endpoints by comparing Spring Boot code with your docs. Optional AI-drafted README fixes, verified against the code.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"docdrift": "bin/docdrift.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test",
|
|
17
|
+
"eval": "node evals/run.js",
|
|
18
|
+
"eval:fix": "node evals/fix-eval.js"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"spring-boot",
|
|
26
|
+
"documentation",
|
|
27
|
+
"docs",
|
|
28
|
+
"cli",
|
|
29
|
+
"rest",
|
|
30
|
+
"drift",
|
|
31
|
+
"endpoints"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/prabhatsingh415/docdrift.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/prabhatsingh415/docdrift#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/prabhatsingh415/docdrift/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/banner.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const WIDTH = 76;
|
|
2
|
+
const LETTER_GAP = ' ';
|
|
3
|
+
const WORD_GAP = ' ';
|
|
4
|
+
|
|
5
|
+
const GLYPHS = {
|
|
6
|
+
D: ['######', '# #', '# #', '# #', '# #', '# #', '######'],
|
|
7
|
+
O: [' #### ', '# #', '# #', '# #', '# #', '# #', ' #### '],
|
|
8
|
+
C: [' #####', '# ', '# ', '# ', '# ', '# ', ' #####'],
|
|
9
|
+
R: ['##### ', '# #', '# #', '##### ', '# # ', '# # ', '# #'],
|
|
10
|
+
I: ['######', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', '######'],
|
|
11
|
+
F: ['######', '# ', '# ', '##### ', '# ', '# ', '# '],
|
|
12
|
+
T: ['######', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## '],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const WORDMARK_ROWS = Array.from({ length: 7 }, (_, row) =>
|
|
16
|
+
['DOC', 'DRIFT']
|
|
17
|
+
.map((word) =>
|
|
18
|
+
word
|
|
19
|
+
.split('')
|
|
20
|
+
.map((letter) => GLYPHS[letter][row])
|
|
21
|
+
.join(LETTER_GAP),
|
|
22
|
+
)
|
|
23
|
+
.join(WORD_GAP),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function centered(text, paint) {
|
|
27
|
+
const padding = Math.max(0, Math.floor((WIDTH - text.length) / 2));
|
|
28
|
+
const right = Math.max(0, WIDTH - padding - text.length);
|
|
29
|
+
return `${' '.repeat(padding)}${paint(text)}${' '.repeat(right)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function paintWordmark(style, text) {
|
|
33
|
+
if (!style.enabled) return text;
|
|
34
|
+
return style.bold(style.success(text));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Full DocDrift branding: the wordmark is the whole identity.
|
|
39
|
+
*
|
|
40
|
+
* @param {ReturnType<import('./style.js').createStyle>} style
|
|
41
|
+
* @param {string} version
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function fullBanner(style, version = '0.0.0') {
|
|
45
|
+
const rows = WORDMARK_ROWS.map((row) =>
|
|
46
|
+
centered(row, (text) => paintWordmark(style, text)),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
rows.push(centered("Code changes. Docs don't.", (text) => style.muted(text)));
|
|
50
|
+
rows.push(centered(`v${version}`, (text) => style.muted(text)));
|
|
51
|
+
|
|
52
|
+
return rows.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One-line branding for the start of colored check/fix runs.
|
|
57
|
+
*/
|
|
58
|
+
export function compactBanner(style, version = '0.0.0') {
|
|
59
|
+
return `${style.bold(style.success('DocDrift'))} ${style.muted(`v${version}`)}`;
|
|
60
|
+
}
|