@curiousnerd/keel 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 +250 -0
- package/data/capability-buckets.json +15 -0
- package/dist/analyze/docDrift.d.ts +9 -0
- package/dist/analyze/docDrift.js +116 -0
- package/dist/analyze/docDrift.js.map +1 -0
- package/dist/analyze/drift.d.ts +4 -0
- package/dist/analyze/drift.js +134 -0
- package/dist/analyze/drift.js.map +1 -0
- package/dist/analyze/duplication.d.ts +7 -0
- package/dist/analyze/duplication.js +46 -0
- package/dist/analyze/duplication.js.map +1 -0
- package/dist/analyze/index.d.ts +10 -0
- package/dist/analyze/index.js +28 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/analyze/libConflicts.d.ts +9 -0
- package/dist/analyze/libConflicts.js +36 -0
- package/dist/analyze/libConflicts.js.map +1 -0
- package/dist/analyze/nearDup.d.ts +11 -0
- package/dist/analyze/nearDup.js +67 -0
- package/dist/analyze/nearDup.js.map +1 -0
- package/dist/analyze/score.d.ts +6 -0
- package/dist/analyze/score.js +39 -0
- package/dist/analyze/score.js.map +1 -0
- package/dist/analyze/shared.d.ts +19 -0
- package/dist/analyze/shared.js +53 -0
- package/dist/analyze/shared.js.map +1 -0
- package/dist/cache/hashCache.d.ts +19 -0
- package/dist/cache/hashCache.js +49 -0
- package/dist/cache/hashCache.js.map +1 -0
- package/dist/claims/parseBlock.d.ts +4 -0
- package/dist/claims/parseBlock.js +66 -0
- package/dist/claims/parseBlock.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +136 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -0
- package/dist/extract/imports.d.ts +12 -0
- package/dist/extract/imports.js +74 -0
- package/dist/extract/imports.js.map +1 -0
- package/dist/extract/index.d.ts +24 -0
- package/dist/extract/index.js +117 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/language.d.ts +3 -0
- package/dist/extract/language.js +13 -0
- package/dist/extract/language.js.map +1 -0
- package/dist/extract/naming.d.ts +11 -0
- package/dist/extract/naming.js +57 -0
- package/dist/extract/naming.js.map +1 -0
- package/dist/extract/packageJson.d.ts +3 -0
- package/dist/extract/packageJson.js +43 -0
- package/dist/extract/packageJson.js.map +1 -0
- package/dist/extract/python.d.ts +11 -0
- package/dist/extract/python.js +244 -0
- package/dist/extract/python.js.map +1 -0
- package/dist/extract/scan.d.ts +12 -0
- package/dist/extract/scan.js +16 -0
- package/dist/extract/scan.js.map +1 -0
- package/dist/extract/symbols.d.ts +9 -0
- package/dist/extract/symbols.js +120 -0
- package/dist/extract/symbols.js.map +1 -0
- package/dist/extract/walk.d.ts +10 -0
- package/dist/extract/walk.js +115 -0
- package/dist/extract/walk.js.map +1 -0
- package/dist/llm/cache.d.ts +17 -0
- package/dist/llm/cache.js +50 -0
- package/dist/llm/cache.js.map +1 -0
- package/dist/llm/claimsFromDocs.d.ts +16 -0
- package/dist/llm/claimsFromDocs.js +95 -0
- package/dist/llm/claimsFromDocs.js.map +1 -0
- package/dist/llm/explain.d.ts +10 -0
- package/dist/llm/explain.js +63 -0
- package/dist/llm/explain.js.map +1 -0
- package/dist/llm/improve.d.ts +9 -0
- package/dist/llm/improve.js +37 -0
- package/dist/llm/improve.js.map +1 -0
- package/dist/llm/provider.d.ts +24 -0
- package/dist/llm/provider.js +210 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +43 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +173 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/report/json.d.ts +3 -0
- package/dist/report/json.js +5 -0
- package/dist/report/json.js.map +1 -0
- package/dist/report/markdown.d.ts +9 -0
- package/dist/report/markdown.js +97 -0
- package/dist/report/markdown.js.map +1 -0
- package/dist/report/text.d.ts +11 -0
- package/dist/report/text.js +76 -0
- package/dist/report/text.js.map +1 -0
- package/dist/suppress.d.ts +22 -0
- package/dist/suppress.js +80 -0
- package/dist/suppress.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/util/fingerprint.d.ts +12 -0
- package/dist/util/fingerprint.js +60 -0
- package/dist/util/fingerprint.js.map +1 -0
- package/dist/util/hash.d.ts +4 -0
- package/dist/util/hash.js +15 -0
- package/dist/util/hash.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aditya
|
|
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
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# keel
|
|
2
|
+
|
|
3
|
+
**A small tool that keeps your AI coding assistant honest.**
|
|
4
|
+
|
|
5
|
+
When you build with an AI agent (Claude Code, Cursor, Copilot…), it forgets things
|
|
6
|
+
between sessions. It picks a different library than the one you agreed on. It writes
|
|
7
|
+
the same helper function twice under two names. It does the opposite of what your
|
|
8
|
+
`CLAUDE.md` says. You often can't see it, because you didn't write the code.
|
|
9
|
+
|
|
10
|
+
keel reads your code and points out exactly where this has happened:
|
|
11
|
+
|
|
12
|
+
> Your `CLAUDE.md` says you use **Zod**. Your code imports **Yup**. You have
|
|
13
|
+
> `formatDate` written twice. You're fetching HTTP with both **axios** and **ky**.
|
|
14
|
+
> **keel finds this in milliseconds, for $0** — no AI, no cloud, no API key.
|
|
15
|
+
|
|
16
|
+
It gives you one number — a **Coherence Score** out of 100 — plus a short list of
|
|
17
|
+
exactly what's wrong and where.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What it catches
|
|
22
|
+
|
|
23
|
+
| | What it means | Example |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| **Drift** | Your notes say one thing, the code does another | "Says Zod, but 7 files import Yup" |
|
|
26
|
+
| **Library conflicts** | Two libraries doing the same job | "axios in 4 files, ky in 1 — pick one" |
|
|
27
|
+
| **Exact duplicates** | The same function written more than once | "`formatDate` and `dateToString` are identical" |
|
|
28
|
+
| **Near duplicates** | Almost-the-same function copy-pasted and tweaked | "`buildSettingsA` is ~91% similar to `buildSettingsB`" |
|
|
29
|
+
| **Doc drift** | Your docs reference files that no longer exist | "`docs/api.md` links to `src/old-parser.ts` — it's gone" |
|
|
30
|
+
|
|
31
|
+
Everything is computed by reading the code directly — no AI model is involved, so the
|
|
32
|
+
results are exact, repeatable, and free.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## What you'll see
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
$ keel check
|
|
40
|
+
|
|
41
|
+
Coherence Score: 84 / 100
|
|
42
|
+
|
|
43
|
+
Drift -7
|
|
44
|
+
Library conflicts -5
|
|
45
|
+
Duplication -4
|
|
46
|
+
|
|
47
|
+
Findings
|
|
48
|
+
✗ DRIFT CLAUDE.md: "validation: Zod" — but code imports yup (1 file)
|
|
49
|
+
✗ CONFLICT http: axios (1 file), ky (1 file)
|
|
50
|
+
✗ DUP formatDate / dateToString are identical implementations
|
|
51
|
+
~ DUP~ buildSettingsA() ~91% similar to buildSettingsB()
|
|
52
|
+
|
|
53
|
+
scanned 5 files · 3.9ms · $0.00
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Loud findings (`✗`) are high-confidence. Gentle findings (`~`) are advisory — they
|
|
57
|
+
might be intentional. keel **never blocks your work** unless you explicitly ask it to.
|
|
58
|
+
|
|
59
|
+
The terminal shows a capped summary. For the **complete** picture — every finding with its
|
|
60
|
+
exact location and cause, plus a prioritized "how to improve" — write a Markdown report:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
keel check --output-md # writes keel-report.md
|
|
64
|
+
keel check --llm --output-md # + plain-English explanations and an AI improvement plan
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The report is written to where you run keel, never into the scanned repo.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Getting started
|
|
72
|
+
|
|
73
|
+
You'll need [Node.js](https://nodejs.org) version 20 or newer.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install -g @curiousnerd/keel
|
|
77
|
+
keel check /path/to/your/project # any JavaScript, TypeScript, or Python project
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
That's it. Run it again any time — it remembers what it already scanned, so repeat
|
|
81
|
+
runs are nearly instant.
|
|
82
|
+
|
|
83
|
+
**From source** (for contributing):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone <this-repo> keel && cd keel
|
|
87
|
+
npm install
|
|
88
|
+
npm run build
|
|
89
|
+
node dist/cli.js check /path/to/your/project
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Commands and options
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
keel check [path] # scan a project (defaults to the current folder)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Option | What it does |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `-v`, `--verbose` | Show extra detail under each finding (file + line). |
|
|
101
|
+
| `--json` | Print the result as JSON, for scripts or CI. |
|
|
102
|
+
| `--output-md [file]` | Write a **full** Markdown report (every finding + locations + causes + how-to-improve) to a file (default `keel-report.md`). |
|
|
103
|
+
| `--limit <n>` | Max findings to print in text mode (default 25). |
|
|
104
|
+
| `--no-cache` | Don't read or write the `.keel` cache — a fully read-only run. |
|
|
105
|
+
| `--no-gitignore` | Scan files even if they're listed in `.gitignore`. |
|
|
106
|
+
| `--no-python` | Skip Python files (don't load the Python parser). |
|
|
107
|
+
| `--llm` | Add plain-language explanations to findings (off by default — see below). |
|
|
108
|
+
| `--fail-under <n>` | Exit with an error if the score is below `<n>`. Off by default. |
|
|
109
|
+
| `--facts` | Print the raw data keel extracted (for debugging). |
|
|
110
|
+
|
|
111
|
+
The score is **graduated**: each category (drift, conflicts, duplication) adds up but
|
|
112
|
+
eases off as problems pile up, so a repo with 30 duplicates scores low without instantly
|
|
113
|
+
hitting zero.
|
|
114
|
+
|
|
115
|
+
keel scans only the code you actually maintain. It **respects your `.gitignore`** (root
|
|
116
|
+
and nested), and on top of that skips generated/minified files (bundles, Prisma runtimes,
|
|
117
|
+
etc.) and `node_modules`, `dist`, `build`, and similar. Duplicate detection only considers
|
|
118
|
+
*named* functions, so inline callbacks don't create noise.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Telling keel about your decisions
|
|
123
|
+
|
|
124
|
+
To catch **drift**, keel needs to know what you decided. Add a short block to your
|
|
125
|
+
`CLAUDE.md` or `AGENTS.md` (keel reads both):
|
|
126
|
+
|
|
127
|
+
```markdown
|
|
128
|
+
## Stack
|
|
129
|
+
- Validation: Zod
|
|
130
|
+
- Database: PostgreSQL
|
|
131
|
+
- Package manager: pnpm
|
|
132
|
+
- Naming: camelCase
|
|
133
|
+
|
|
134
|
+
## Constraints
|
|
135
|
+
- never use `any`
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
keel checks each line it understands against the real code. Lines it can't check, it
|
|
139
|
+
simply ignores — it never guesses.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Silencing a finding you don't care about
|
|
144
|
+
|
|
145
|
+
False alarms are the fastest way to make a tool annoying, so keel makes them easy to
|
|
146
|
+
silence — three ways, from most specific to broadest:
|
|
147
|
+
|
|
148
|
+
**1. A comment in the code** (best for an intentional duplicate):
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// keel-ignore: this copy is intentional
|
|
152
|
+
export function dateToString(value: Date): string { /* ... */ }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**2. A `.keelignore` file** in your project root:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
# We're mid-migration from axios to ky — don't flag it yet.
|
|
159
|
+
bucket:http
|
|
160
|
+
|
|
161
|
+
# Don't look at old code.
|
|
162
|
+
src/legacy/**
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**3. The config file** `keel.config.json`:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"duplication": {
|
|
170
|
+
"minTokens": 20,
|
|
171
|
+
"near": { "enabled": true, "threshold": 0.85 }
|
|
172
|
+
},
|
|
173
|
+
"libConflicts": { "ignoreBuckets": ["test"] }
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- `minTokens` — ignore functions smaller than this (avoids flagging tiny one-liners).
|
|
178
|
+
- `near.threshold` — how similar (0–1) two functions must be to count as near-duplicates.
|
|
179
|
+
Higher = stricter. `0.85` is the conservative default.
|
|
180
|
+
- `ignoreBuckets` — capability groups (like `http`, `date`) to never flag.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Why no AI?
|
|
185
|
+
|
|
186
|
+
It would seem natural for a tool like this to *use* an AI model. It deliberately
|
|
187
|
+
doesn't. A study from ETH Zurich found that AI-**generated** context files actually
|
|
188
|
+
made coding agents *worse* — lower success rate, higher cost. So keel does the
|
|
189
|
+
opposite: it reads the real code with plain static analysis. The answers are exact and
|
|
190
|
+
cost nothing.
|
|
191
|
+
|
|
192
|
+
The *detection* is always deterministic. `keel check --llm` adds two **opt-in** extras:
|
|
193
|
+
|
|
194
|
+
1. A one-line plain-English **explanation** under each finding.
|
|
195
|
+
2. **Doc-claim drift** — it reads your README/CLAUDE.md *prose* (not just a structured
|
|
196
|
+
block), extracts the stack you say you use ("validation: Zod", "database: Postgres"),
|
|
197
|
+
and checks it against the code. Here the LLM only turns English into a claim — the same
|
|
198
|
+
**deterministic** drift engine does the verifying, so **the score never depends on the
|
|
199
|
+
LLM's judgement** and stays reproducible whether the LLM is on or off. By default it shells out to the `claude` or `codex` CLI you already
|
|
200
|
+
have (no API key, no separate bill); set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` to use an
|
|
201
|
+
API instead. Responses are cached, and if nothing's available keel just skips them.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Use it as an MCP server (catch problems *before* they're written)
|
|
206
|
+
|
|
207
|
+
`keel check` is a verifier you run *after* the fact. The MCP server flips that around:
|
|
208
|
+
it lets your AI agent ask keel **before** it writes code, so duplication and
|
|
209
|
+
inconsistency are prevented at generation time instead of caught later. It exposes three
|
|
210
|
+
tools, all answered from live, deterministic facts about your code — **no AI, no cost**:
|
|
211
|
+
|
|
212
|
+
| Tool | What the agent gets |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `check_before_write(intent)` | Existing functions it might be about to reinvent, and the libraries already in use for that job ("you already have `formatDate`; you already use axios — don't add ky"). |
|
|
215
|
+
| `get_conventions(path)` | The real conventions for that area — naming style, language, libraries in use — derived from the code, not a stale doc. |
|
|
216
|
+
| `report_drift()` | The project's current drift + library conflicts, so the agent doesn't add to them. |
|
|
217
|
+
|
|
218
|
+
**Register it with Claude Code** (after `npm install -g @curiousnerd/keel`):
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
claude mcp add keel -- keel mcp
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Or drop a `.mcp.json` in your project (works in Cursor and other MCP hosts too):
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{ "mcpServers": { "keel": { "command": "keel", "args": ["mcp"] } } }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The server serves the current directory by default; pass a path (`mcp /some/repo`) to
|
|
231
|
+
serve another. It re-reads the code on every call (the file-hash cache keeps that
|
|
232
|
+
near-instant), so answers always reflect the current state.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Status
|
|
237
|
+
|
|
238
|
+
**Working today.** Drift + doc drift, library conflicts, exact + near duplicates,
|
|
239
|
+
Coherence Score, suppressions, caching, an opt-in LLM layer, and an **MCP server** —
|
|
240
|
+
across **JavaScript, TypeScript, and Python**. Python is parsed with tree-sitter
|
|
241
|
+
(WebAssembly), so there's nothing to compile at install time.
|
|
242
|
+
|
|
243
|
+
**Coming next:** a pre-commit hook and GitHub Action · semantic duplication · more
|
|
244
|
+
languages.
|
|
245
|
+
|
|
246
|
+
Want to help or understand the internals? See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"http": ["axios", "got", "node-fetch", "ky", "superagent", "request", "undici", "requests", "httpx", "aiohttp", "urllib3", "httplib2"],
|
|
3
|
+
"date": ["moment", "dayjs", "date-fns", "luxon", "arrow", "pendulum", "dateutil"],
|
|
4
|
+
"validation": ["zod", "yup", "joi", "ajv", "superstruct", "valibot", "pydantic", "marshmallow", "cerberus", "voluptuous", "schema"],
|
|
5
|
+
"state": ["redux", "zustand", "jotai", "recoil", "mobx", "@reduxjs/toolkit"],
|
|
6
|
+
"orm": ["prisma", "drizzle-orm", "typeorm", "sequelize", "knex", "mongoose", "sqlalchemy", "peewee", "tortoise", "pony"],
|
|
7
|
+
"test": ["jest", "vitest", "mocha", "ava", "jasmine", "tape", "pytest", "nose", "nose2"],
|
|
8
|
+
"logging": ["winston", "pino", "bunyan", "loglevel", "signale", "loguru", "structlog"],
|
|
9
|
+
"env": ["dotenv", "dotenv-flow", "env-var", "envalid", "environs", "decouple"],
|
|
10
|
+
"uuid": ["uuid", "nanoid", "cuid", "ulid"],
|
|
11
|
+
"styling": ["styled-components", "@emotion/react", "tailwindcss"],
|
|
12
|
+
"forms": ["react-hook-form", "formik", "react-final-form"],
|
|
13
|
+
"data-fetch": ["@tanstack/react-query", "swr", "apollo-client", "@apollo/client", "urql"],
|
|
14
|
+
"web-framework": ["express", "fastify", "koa", "hapi", "flask", "django", "fastapi", "tornado", "bottle", "sanic", "falcon"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { KeelConfig } from "../config.js";
|
|
2
|
+
import type { Facts, Finding } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic doc/knowledge drift: flag file-path references in Markdown docs
|
|
5
|
+
* that no longer exist in the repo. High precision — only concrete, local,
|
|
6
|
+
* checkable paths are verified; URLs, placeholders, and build/runtime paths are
|
|
7
|
+
* skipped, and a line carrying `keel-ignore` suppresses its references.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectDocDrift(facts: Facts, config: KeelConfig): Finding[];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { DEFAULT_IGNORE_DIRS, walkMarkdownFiles } from "../extract/walk.js";
|
|
4
|
+
const DOC_REF_PENALTY = 4;
|
|
5
|
+
/** File extensions worth verifying — avoids flagging prose like `example.com` or `v1.2`. */
|
|
6
|
+
const CHECKABLE_EXT = new Set([
|
|
7
|
+
"ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "rs", "rb", "java", "kt", "php", "cs",
|
|
8
|
+
"c", "cpp", "h", "hpp", "swift", "scala", "md", "mdx", "json", "yaml", "yml", "toml",
|
|
9
|
+
"ini", "env", "sh", "bash", "css", "scss", "less", "html", "xml", "sql", "prisma",
|
|
10
|
+
"graphql", "gql", "vue", "svelte", "proto", "lock", "cfg", "conf",
|
|
11
|
+
]);
|
|
12
|
+
function hasCheckableExtension(p) {
|
|
13
|
+
const m = /\.([A-Za-z0-9]+)$/.exec(p);
|
|
14
|
+
return m !== null && CHECKABLE_EXT.has(m[1].toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
/** URL, mail/tel, protocol-relative, or pure in-page anchor — not a repo path. */
|
|
17
|
+
function isUrlOrAnchor(s) {
|
|
18
|
+
return /^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i.test(s);
|
|
19
|
+
}
|
|
20
|
+
/** Placeholder/glob/template markers mean it isn't a concrete path. */
|
|
21
|
+
function hasPlaceholder(s) {
|
|
22
|
+
return /[<>{}*]|\.\.\./.test(s);
|
|
23
|
+
}
|
|
24
|
+
/** Names like `yourCheck.ts`, `myModule.ts`, `foo.ts` are doc templates, not real files. */
|
|
25
|
+
function looksTemplated(ref) {
|
|
26
|
+
const base = (ref.split("/").pop() ?? "").replace(/\.[A-Za-z0-9]+$/, "");
|
|
27
|
+
if (/^(?:your|my)[A-Z]/.test(base))
|
|
28
|
+
return true; // yourCheck, myModule
|
|
29
|
+
return ["foo", "bar", "baz", "qux", "example", "sample", "placeholder"].includes(base.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
/** First path segment, for skipping references into build/runtime/ignored dirs. */
|
|
32
|
+
function firstSegment(ref) {
|
|
33
|
+
return ref.replace(/^\.?\//, "").split("/")[0] ?? "";
|
|
34
|
+
}
|
|
35
|
+
/** Extract candidate repo-path references (markdown links + inline code spans) from one line. */
|
|
36
|
+
function refsInLine(line, lineNo) {
|
|
37
|
+
const refs = [];
|
|
38
|
+
// Markdown links: [text](target)
|
|
39
|
+
for (const m of line.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
40
|
+
const target = m[1].trim().split(/[#?\s]/)[0] ?? "";
|
|
41
|
+
if (target &&
|
|
42
|
+
!isUrlOrAnchor(target) &&
|
|
43
|
+
!hasPlaceholder(target) &&
|
|
44
|
+
!looksTemplated(target) &&
|
|
45
|
+
hasCheckableExtension(target)) {
|
|
46
|
+
refs.push({ ref: target, line: lineNo });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Inline code spans: `path/to/file.ext` — require a slash to stay high-precision.
|
|
50
|
+
for (const m of line.matchAll(/`([^`]+)`/g)) {
|
|
51
|
+
const span = m[1].trim();
|
|
52
|
+
if (span.includes("/") &&
|
|
53
|
+
!/\s/.test(span) &&
|
|
54
|
+
!isUrlOrAnchor(span) &&
|
|
55
|
+
!hasPlaceholder(span) &&
|
|
56
|
+
!looksTemplated(span) &&
|
|
57
|
+
hasCheckableExtension(span)) {
|
|
58
|
+
refs.push({ ref: span, line: lineNo });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return refs;
|
|
62
|
+
}
|
|
63
|
+
/** Does `ref` resolve to a file, relative to either the repo root or the doc's directory? */
|
|
64
|
+
function refResolves(root, docDir, ref) {
|
|
65
|
+
const clean = ref.replace(/^\.\//, "");
|
|
66
|
+
return existsSync(join(root, clean)) || existsSync(join(root, docDir, clean));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Deterministic doc/knowledge drift: flag file-path references in Markdown docs
|
|
70
|
+
* that no longer exist in the repo. High precision — only concrete, local,
|
|
71
|
+
* checkable paths are verified; URLs, placeholders, and build/runtime paths are
|
|
72
|
+
* skipped, and a line carrying `keel-ignore` suppresses its references.
|
|
73
|
+
*/
|
|
74
|
+
export function detectDocDrift(facts, config) {
|
|
75
|
+
if (!config.docs.enabled)
|
|
76
|
+
return [];
|
|
77
|
+
const root = facts.root;
|
|
78
|
+
const docs = walkMarkdownFiles(root, { respectGitignore: config.scan.respectGitignore });
|
|
79
|
+
const findings = [];
|
|
80
|
+
for (const docPath of docs) {
|
|
81
|
+
let content;
|
|
82
|
+
try {
|
|
83
|
+
content = readFileSync(join(root, docPath), "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const docDir = dirname(docPath);
|
|
89
|
+
const lines = content.split(/\r?\n/);
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const line = lines[i];
|
|
93
|
+
if (/keel-ignore/i.test(line))
|
|
94
|
+
continue; // inline suppression
|
|
95
|
+
for (const { ref, line: lineNo } of refsInLine(line, i + 1)) {
|
|
96
|
+
if (DEFAULT_IGNORE_DIRS.has(firstSegment(ref)))
|
|
97
|
+
continue; // build/runtime artifact
|
|
98
|
+
if (seen.has(ref))
|
|
99
|
+
continue;
|
|
100
|
+
if (refResolves(root, docDir, ref))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(ref);
|
|
103
|
+
findings.push({
|
|
104
|
+
kind: "drift",
|
|
105
|
+
confidence: "high",
|
|
106
|
+
title: `${docPath}: references missing path \`${ref}\``,
|
|
107
|
+
detail: `The docs point at \`${ref}\`, but no such file exists in the repo — it was likely moved, renamed, or deleted.`,
|
|
108
|
+
location: `${docPath}:${lineNo}`,
|
|
109
|
+
penalty: DOC_REF_PENALTY,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return findings;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=docDrift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docDrift.js","sourceRoot":"","sources":["../../src/analyze/docDrift.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Claims, Facts, Finding } from "../types.js";
|
|
2
|
+
import { type CapabilityBuckets } from "./shared.js";
|
|
3
|
+
/** Compare parsed Claims against extracted Facts and emit drift findings. */
|
|
4
|
+
export declare function detectDrift(claims: Claims, facts: Facts, buckets?: CapabilityBuckets): Finding[];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { filesByPackage, loadCapabilityBuckets } from "./shared.js";
|
|
2
|
+
const LIB_PENALTY = 8;
|
|
3
|
+
const NAMING_PENALTY = 5;
|
|
4
|
+
/** Known database drivers grouped by family, for the "declared DB" check. */
|
|
5
|
+
const DB_FAMILIES = {
|
|
6
|
+
postgres: ["pg", "postgres", "pg-promise", "postgresql"],
|
|
7
|
+
sqlite: ["sqlite3", "better-sqlite3"],
|
|
8
|
+
mysql: ["mysql", "mysql2"],
|
|
9
|
+
mongodb: ["mongodb", "mongoose"],
|
|
10
|
+
mssql: ["mssql", "tedious"],
|
|
11
|
+
};
|
|
12
|
+
function dbFamilyOf(value) {
|
|
13
|
+
const v = value.toLowerCase();
|
|
14
|
+
if (v.includes("postgres"))
|
|
15
|
+
return "postgres";
|
|
16
|
+
if (v.includes("sqlite"))
|
|
17
|
+
return "sqlite";
|
|
18
|
+
if (v.includes("mysql") || v.includes("maria"))
|
|
19
|
+
return "mysql";
|
|
20
|
+
if (v.includes("mongo"))
|
|
21
|
+
return "mongodb";
|
|
22
|
+
if (v.includes("mssql") || v.includes("sql server"))
|
|
23
|
+
return "mssql";
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const countFor = (byPkg, pkg) => byPkg.get(pkg)?.length ?? 0;
|
|
27
|
+
const summarize = (entries) => entries.map((e) => `${e.pkg} (${e.n} file${e.n === 1 ? "" : "s"})`).join(", ");
|
|
28
|
+
function checkLibrary(claim, byPkg, buckets) {
|
|
29
|
+
const value = claim.value.toLowerCase().trim();
|
|
30
|
+
for (const [, members] of Object.entries(buckets)) {
|
|
31
|
+
if (!members.includes(value))
|
|
32
|
+
continue;
|
|
33
|
+
if (countFor(byPkg, value) > 0)
|
|
34
|
+
return null; // claimed lib is actually used
|
|
35
|
+
const competitors = members
|
|
36
|
+
.filter((m) => m !== value && countFor(byPkg, m) > 0)
|
|
37
|
+
.map((m) => ({ pkg: m, n: countFor(byPkg, m) }));
|
|
38
|
+
if (competitors.length === 0)
|
|
39
|
+
return null; // nothing contradicts it
|
|
40
|
+
return {
|
|
41
|
+
kind: "drift",
|
|
42
|
+
confidence: "high",
|
|
43
|
+
title: `${claim.source}: "${claim.key}: ${claim.value}" — but code imports ${summarize(competitors)}`,
|
|
44
|
+
detail: `Declared ${claim.key} is "${claim.value}" but it is not imported anywhere; ${competitors[0].pkg} is used instead.`,
|
|
45
|
+
location: `${claim.source}:${claim.line}`,
|
|
46
|
+
penalty: LIB_PENALTY,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function checkDatabase(claim, byPkg) {
|
|
52
|
+
const claimedFamily = dbFamilyOf(claim.value);
|
|
53
|
+
if (!claimedFamily)
|
|
54
|
+
return null;
|
|
55
|
+
const isImported = (family) => DB_FAMILIES[family].some((d) => countFor(byPkg, d) > 0);
|
|
56
|
+
if (isImported(claimedFamily))
|
|
57
|
+
return null;
|
|
58
|
+
const others = Object.keys(DB_FAMILIES).filter((f) => f !== claimedFamily && isImported(f));
|
|
59
|
+
if (others.length === 0)
|
|
60
|
+
return null; // can't verify — no DB driver imported
|
|
61
|
+
const drivers = others.flatMap((f) => DB_FAMILIES[f].filter((d) => countFor(byPkg, d) > 0));
|
|
62
|
+
return {
|
|
63
|
+
kind: "drift",
|
|
64
|
+
confidence: "high",
|
|
65
|
+
title: `${claim.source}: "${claim.key}: ${claim.value}" — but code imports ${drivers.join(", ")}`,
|
|
66
|
+
location: `${claim.source}:${claim.line}`,
|
|
67
|
+
penalty: LIB_PENALTY,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function checkPackageManager(claim, facts) {
|
|
71
|
+
if (!facts.pkg || facts.pkg.packageManager === "unknown")
|
|
72
|
+
return null; // unverifiable
|
|
73
|
+
const claimed = claim.value.toLowerCase().trim().split(/\s|@/)[0];
|
|
74
|
+
const detected = facts.pkg.packageManager;
|
|
75
|
+
if (claimed === detected)
|
|
76
|
+
return null;
|
|
77
|
+
return {
|
|
78
|
+
kind: "drift",
|
|
79
|
+
confidence: "high",
|
|
80
|
+
title: `${claim.source}: "${claim.key}: ${claim.value}" — but lockfile indicates ${detected}`,
|
|
81
|
+
location: `${claim.source}:${claim.line}`,
|
|
82
|
+
penalty: LIB_PENALTY,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function checkNaming(claim, facts) {
|
|
86
|
+
const v = claim.value.toLowerCase();
|
|
87
|
+
// The casing that VIOLATES the claimed convention. Single lowercase words
|
|
88
|
+
// (`lower`) are valid under both camelCase and snake_case, so they're neutral.
|
|
89
|
+
const violatingStyle = v.includes("camel")
|
|
90
|
+
? "snake_case"
|
|
91
|
+
: v.includes("snake")
|
|
92
|
+
? "camelCase"
|
|
93
|
+
: null;
|
|
94
|
+
if (!violatingStyle)
|
|
95
|
+
return null;
|
|
96
|
+
let compatible = 0; // names consistent with the claim (claimed style + neutral)
|
|
97
|
+
let violating = 0;
|
|
98
|
+
for (const f of facts.files) {
|
|
99
|
+
violating += f.naming[violatingStyle] ?? 0;
|
|
100
|
+
compatible += (f.naming.lower ?? 0) + (violatingStyle === "snake_case" ? f.naming.camelCase ?? 0 : f.naming.snake_case ?? 0);
|
|
101
|
+
}
|
|
102
|
+
if (compatible + violating < 10)
|
|
103
|
+
return null; // too little signal
|
|
104
|
+
if (!(violating > compatible))
|
|
105
|
+
return null; // claimed style prevails (also guards NaN)
|
|
106
|
+
return {
|
|
107
|
+
kind: "drift",
|
|
108
|
+
confidence: "medium",
|
|
109
|
+
title: `${claim.source}: "${claim.key}: ${claim.value}" — but ${violatingStyle} prevails (${violating} vs ${compatible})`,
|
|
110
|
+
location: `${claim.source}:${claim.line}`,
|
|
111
|
+
penalty: NAMING_PENALTY,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Compare parsed Claims against extracted Facts and emit drift findings. */
|
|
115
|
+
export function detectDrift(claims, facts, buckets = loadCapabilityBuckets()) {
|
|
116
|
+
const byPkg = filesByPackage(facts);
|
|
117
|
+
const findings = [];
|
|
118
|
+
for (const claim of claims.stack) {
|
|
119
|
+
const key = claim.key;
|
|
120
|
+
let finding = null;
|
|
121
|
+
if (key.includes("package manager"))
|
|
122
|
+
finding = checkPackageManager(claim, facts);
|
|
123
|
+
else if (key === "database" || key === "db")
|
|
124
|
+
finding = checkDatabase(claim, byPkg);
|
|
125
|
+
else if (key.includes("naming"))
|
|
126
|
+
finding = checkNaming(claim, facts);
|
|
127
|
+
else
|
|
128
|
+
finding = checkLibrary(claim, byPkg, buckets);
|
|
129
|
+
if (finding)
|
|
130
|
+
findings.push(finding);
|
|
131
|
+
}
|
|
132
|
+
return findings;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drift.js","sourceRoot":"","sources":["../../src/analyze/drift.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Facts, Finding } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Group functions by normalized-body hash; any group of 2+ is an exact-clone
|
|
4
|
+
* finding. Trivial bodies were already excluded at extraction (null hash).
|
|
5
|
+
* Near-duplicate detection is deferred to v0.5.
|
|
6
|
+
*/
|
|
7
|
+
export declare function detectDuplication(facts: Facts): Finding[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { buildFunctionIgnoreCheck } from "./shared.js";
|
|
2
|
+
const DUP_PENALTY = 4;
|
|
3
|
+
/**
|
|
4
|
+
* Group functions by normalized-body hash; any group of 2+ is an exact-clone
|
|
5
|
+
* finding. Trivial bodies were already excluded at extraction (null hash).
|
|
6
|
+
* Near-duplicate detection is deferred to v0.5.
|
|
7
|
+
*/
|
|
8
|
+
export function detectDuplication(facts) {
|
|
9
|
+
// A function is suppressed when its declaration line (or the line above it)
|
|
10
|
+
// carries an inline `// keel-ignore` marker.
|
|
11
|
+
const isSuppressed = buildFunctionIgnoreCheck(facts);
|
|
12
|
+
const groups = new Map();
|
|
13
|
+
for (const file of facts.files) {
|
|
14
|
+
for (const fn of file.functions) {
|
|
15
|
+
// Only named functions/methods — anonymous inline callbacks aren't
|
|
16
|
+
// "utilities you reinvented", and produce unactionable noise.
|
|
17
|
+
if (fn.bodyHash === null || fn.name === null || isSuppressed(fn))
|
|
18
|
+
continue;
|
|
19
|
+
let group = groups.get(fn.bodyHash);
|
|
20
|
+
if (!group)
|
|
21
|
+
groups.set(fn.bodyHash, (group = []));
|
|
22
|
+
group.push(fn);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const findings = [];
|
|
26
|
+
for (const group of groups.values()) {
|
|
27
|
+
if (group.length < 2)
|
|
28
|
+
continue;
|
|
29
|
+
const locations = group.map((fn) => `${fn.name ?? "(anonymous)"} @ ${fn.filePath}:${fn.startLine}`);
|
|
30
|
+
const names = [...new Set(group.map((fn) => fn.name ?? "(anonymous)"))];
|
|
31
|
+
const title = names.length === 1
|
|
32
|
+
? `${names[0]}() duplicated across ${group.length} locations`
|
|
33
|
+
: `${names.join(" / ")} are identical implementations`;
|
|
34
|
+
findings.push({
|
|
35
|
+
kind: "dup",
|
|
36
|
+
confidence: "high",
|
|
37
|
+
title,
|
|
38
|
+
detail: locations.join("\n "),
|
|
39
|
+
location: `${group[0].filePath}:${group[0].startLine}`,
|
|
40
|
+
penalty: DUP_PENALTY,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Stable order: most-duplicated first, then by title.
|
|
44
|
+
return findings.sort((a, b) => (b.penalty - a.penalty) || a.title.localeCompare(b.title));
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=duplication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplication.js","sourceRoot":"","sources":["../../src/analyze/duplication.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAY;IAC5C,4EAA4E;IAC5E,6CAA6C;IAC7C,MAAM,YAAY,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAErD,MAAM,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC/B,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAChC,mEAAmE;YACnE,8DAA8D;YAC9D,IAAI,EAAE,CAAC,QAAQ,KAAK,IAAI,IAAI,EAAE,CAAC,IAAI,KAAK,IAAI,IAAI,YAAY,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC3E,IAAI,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;YACpC,IAAI,CAAC,KAAK;gBAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC;YAClD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QAE/B,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,aAAa,MAAM,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC;QACpG,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,IAAI,aAAa,CAAC,CAAC,CAAC,CAAC;QACxE,MAAM,KAAK,GACT,KAAK,CAAC,MAAM,KAAK,CAAC;YAChB,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,wBAAwB,KAAK,CAAC,MAAM,YAAY;YAC7D,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,gCAAgC,CAAC;QAE3D,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,KAAK;YACX,UAAU,EAAE,MAAM;YAClB,KAAK;YACL,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC;YACvC,QAAQ,EAAE,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAE,CAAC,SAAS,EAAE;YACxD,OAAO,EAAE,WAAW;SACrB,CAAC,CAAC;IACL,CAAC;IAED,sDAAsD;IACtD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5F,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { KeelConfig } from "../config.js";
|
|
2
|
+
import type { Claims, Facts, Finding, ScoreBreakdown } from "../types.js";
|
|
3
|
+
import { type Suppressions } from "../suppress.js";
|
|
4
|
+
export interface Analysis {
|
|
5
|
+
findings: Finding[];
|
|
6
|
+
score: number;
|
|
7
|
+
breakdown: ScoreBreakdown;
|
|
8
|
+
}
|
|
9
|
+
/** Run every v0 engine against the Facts + Claims, applying suppressions. */
|
|
10
|
+
export declare function analyze(facts: Facts, claims: Claims, config: KeelConfig, suppressions?: Suppressions): Analysis;
|