@kentwynn/kgraph 0.1.14 → 0.1.16
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 +184 -177
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +2 -2
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +100 -0
- package/dist/cli/commands/workflow.d.ts +1 -0
- package/dist/cli/commands/workflow.js +42 -0
- package/dist/cli/help.js +12 -6
- package/dist/cli/index.js +9 -4
- package/dist/cognition/cognition-updater.d.ts +6 -6
- package/dist/cognition/cognition-updater.js +45 -20
- package/dist/cognition/markdown-note-parser.js +7 -18
- package/dist/context/context-query.js +26 -1
- package/dist/scanner/repo-scanner.js +63 -1
- package/dist/storage/cognition-store.js +31 -5
- package/dist/visualization/graph-builder.d.ts +1 -1
- package/dist/visualization/graph-builder.js +46 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,112 +1,71 @@
|
|
|
1
1
|
# KGraph
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> Stop paying the context tax on every session. KGraph gives your AI assistant a memory.
|
|
3
|
+
Persistent repository intelligence for AI coding tools.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
KGraph gives Codex, GitHub Copilot, Cursor, and Claude Code a local knowledge layer for your repo: file maps, symbols, imports, relationships, and durable notes from previous AI sessions. The goal is simple: your assistant should not spend every session re-learning the same codebase.
|
|
7
6
|
|
|
8
|
-
## The
|
|
7
|
+
## The Workflow
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
Use KGraph in two steps:
|
|
11
10
|
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"Let me find where sessions are created..."
|
|
16
|
-
"Let me understand the database layer..."
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
On a medium-sized codebase, this exploration burns **3,000–8,000 tokens** before the AI writes a single line of code. Multiply that across 10 sessions per day and you're spending the majority of your context budget re-learning things you already know.
|
|
20
|
-
|
|
21
|
-
Worse: AI tools forget. The debugging insight from Tuesday, the architecture decision from last sprint, the "don't touch this or it breaks payments" gotcha — gone after every session.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## The Solution
|
|
26
|
-
|
|
27
|
-
KGraph builds a local knowledge layer that grows with your project. It maps your codebase once, captures reasoning from your AI sessions, and serves compact, targeted context on demand.
|
|
11
|
+
```bash
|
|
12
|
+
# Required once per repository
|
|
13
|
+
kgraph init --integrations codex,copilot,cursor,claude-code
|
|
28
14
|
|
|
15
|
+
# Normal daily command
|
|
16
|
+
kgraph "auth token refresh"
|
|
29
17
|
```
|
|
30
|
-
Without KGraph With KGraph
|
|
31
|
-
───────────────────────────────── ──────────────────────────────────
|
|
32
|
-
Session start: ~5,000 tokens Session start: ~300 tokens
|
|
33
|
-
exploring files and structure kgraph context "auth token refresh"
|
|
34
|
-
|
|
35
|
-
Re-learns same architecture Recalls prior decisions instantly
|
|
36
|
-
every single session from cognition store
|
|
37
18
|
|
|
38
|
-
|
|
19
|
+
That second command runs the full practical workflow:
|
|
39
20
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
1. Refreshes the repository scan.
|
|
22
|
+
2. Updates file, symbol, import, and relationship maps.
|
|
23
|
+
3. Processes any Markdown notes waiting in `.kgraph/inbox/`.
|
|
24
|
+
4. Returns compact context for the topic you asked about.
|
|
43
25
|
|
|
44
|
-
|
|
26
|
+
You can also run just:
|
|
45
27
|
|
|
46
|
-
|
|
28
|
+
```bash
|
|
29
|
+
kgraph
|
|
30
|
+
```
|
|
47
31
|
|
|
48
|
-
|
|
32
|
+
That refreshes maps and cognition without printing topic-specific context.
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
topic: auth token refresh
|
|
34
|
+
The smaller commands, such as `kgraph scan`, `kgraph update`, and `kgraph context`, still exist. They are useful when you want one specific step, but they are not the main workflow.
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
src/lib/auth.ts (createSession, validateToken, refreshToken)
|
|
55
|
-
app/api/auth/route.ts (POST handler → createSession)
|
|
56
|
-
middleware.ts (reads session cookie, calls validateToken)
|
|
36
|
+
## Why It Exists
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
POST /api/auth → createSession → writes JWT to cookie
|
|
60
|
-
middleware → validateToken → redirects on expiry
|
|
38
|
+
Most AI coding sessions start like this:
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
40
|
+
```text
|
|
41
|
+
Let me inspect package.json.
|
|
42
|
+
Let me search for auth routes.
|
|
43
|
+
Let me trace imports.
|
|
44
|
+
Let me understand where sessions are stored.
|
|
66
45
|
```
|
|
67
46
|
|
|
68
|
-
|
|
69
|
-
That's a **15x reduction** in context cost for navigation alone.
|
|
47
|
+
That exploration is useful once. It is wasteful the tenth time.
|
|
70
48
|
|
|
71
|
-
|
|
49
|
+
KGraph stores the reusable parts locally:
|
|
72
50
|
|
|
73
|
-
|
|
51
|
+
- What files exist and what language they use.
|
|
52
|
+
- What symbols each source file defines.
|
|
53
|
+
- Which files import each other.
|
|
54
|
+
- Which notes, decisions, debugging findings, and gotchas were captured from prior sessions.
|
|
55
|
+
- Which cognition references are current, mixed, stale, or unresolved after code moves.
|
|
74
56
|
|
|
75
|
-
|
|
57
|
+
Then an AI assistant can ask for focused context before broad exploration:
|
|
76
58
|
|
|
59
|
+
```bash
|
|
60
|
+
kgraph "blog admin token usage"
|
|
77
61
|
```
|
|
78
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
79
|
-
│ Your Codebase │
|
|
80
|
-
└──────────────────────────────┬──────────────────────────────┘
|
|
81
|
-
│ kgraph scan
|
|
82
|
-
▼
|
|
83
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
84
|
-
│ .kgraph/ │
|
|
85
|
-
│ ├── maps/ file graph, symbol index, imports │
|
|
86
|
-
│ ├── cognition.md decisions, gotchas, debugging history │
|
|
87
|
-
│ └── config.yaml include/exclude rules │
|
|
88
|
-
└──────────────────────────────┬──────────────────────────────┘
|
|
89
|
-
│ kgraph context "topic"
|
|
90
|
-
▼
|
|
91
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
92
|
-
│ AI Tool (Copilot / Codex / Cursor / Claude Code) │
|
|
93
|
-
│ Reads compact context → navigates directly → works faster │
|
|
94
|
-
└─────────────────────────────────────────────────────────────┘
|
|
95
|
-
│ session ends
|
|
96
|
-
▼
|
|
97
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
98
|
-
│ .kgraph/inbox/ AI drops a note: what it learned │
|
|
99
|
-
│ kgraph update → distilled into │
|
|
100
|
-
│ cognition.md for the next session │
|
|
101
|
-
└─────────────────────────────────────────────────────────────┘
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
This creates a **compounding feedback loop**: the more you use KGraph, the richer the cognition store, the less exploration the AI needs to do.
|
|
105
62
|
|
|
106
|
-
|
|
63
|
+
Instead of reading the whole repo, it gets a compact starting point: relevant files, symbols, relationships, domains, prior notes, and stale references to watch.
|
|
107
64
|
|
|
108
65
|
## Install
|
|
109
66
|
|
|
67
|
+
Use the published CLI:
|
|
68
|
+
|
|
110
69
|
```bash
|
|
111
70
|
npm install -g @kentwynn/kgraph@latest
|
|
112
71
|
kgraph --version
|
|
@@ -116,107 +75,171 @@ Or run without installing:
|
|
|
116
75
|
|
|
117
76
|
```bash
|
|
118
77
|
npx @kentwynn/kgraph@latest init
|
|
78
|
+
npx @kentwynn/kgraph@latest "auth token refresh"
|
|
119
79
|
```
|
|
120
80
|
|
|
121
|
-
|
|
81
|
+
KGraph requires Node.js 20 or newer.
|
|
122
82
|
|
|
123
83
|
## Quick Start
|
|
124
84
|
|
|
85
|
+
From the root of a repository:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# 1. Create the local KGraph workspace
|
|
89
|
+
kgraph init
|
|
90
|
+
|
|
91
|
+
# 2. Optional: connect AI tools so they know the KGraph workflow
|
|
92
|
+
kgraph integrate add codex copilot cursor claude-code
|
|
93
|
+
|
|
94
|
+
# 3. Run the normal workflow for a topic
|
|
95
|
+
kgraph "auth token refresh"
|
|
96
|
+
|
|
97
|
+
# 4. Check health if something feels off
|
|
98
|
+
kgraph doctor
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
After useful AI work, assistants can save durable notes into `.kgraph/inbox/`. The next `kgraph` run processes those notes automatically. You can also process them directly with `kgraph update`.
|
|
102
|
+
|
|
103
|
+
## Main Commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
kgraph init
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Required once per repo. Creates `.kgraph/` and the local config.
|
|
110
|
+
|
|
125
111
|
```bash
|
|
126
|
-
# 1. Initialize and connect your AI tools
|
|
127
112
|
kgraph init --integrations codex,copilot,cursor,claude-code
|
|
113
|
+
```
|
|
128
114
|
|
|
129
|
-
|
|
130
|
-
kgraph scan
|
|
115
|
+
Initializes KGraph and writes local instruction files for supported AI tools.
|
|
131
116
|
|
|
132
|
-
|
|
133
|
-
kgraph
|
|
117
|
+
```bash
|
|
118
|
+
kgraph "some topic"
|
|
119
|
+
```
|
|
134
120
|
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
The normal command. Scans the repo, updates cognition, and returns focused context for the topic.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
kgraph
|
|
137
125
|
```
|
|
138
126
|
|
|
139
|
-
|
|
127
|
+
Refreshes maps and cognition without returning topic-specific context.
|
|
140
128
|
|
|
141
|
-
|
|
129
|
+
```bash
|
|
130
|
+
kgraph doctor
|
|
131
|
+
```
|
|
142
132
|
|
|
143
|
-
|
|
133
|
+
Checks whether the workspace is initialized, maps exist, inbox notes are pending, and configured integrations point to real files.
|
|
144
134
|
|
|
145
|
-
|
|
135
|
+
## Optional Step Commands
|
|
136
|
+
|
|
137
|
+
These are useful for scripting, debugging, or when you want a single operation.
|
|
146
138
|
|
|
147
139
|
```bash
|
|
148
|
-
kgraph
|
|
149
|
-
|
|
140
|
+
kgraph scan
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Refresh only the structural maps in `.kgraph/map/`.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
kgraph context "auth token refresh"
|
|
147
|
+
kgraph context "auth token refresh" --json
|
|
150
148
|
```
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
| -------------- | --------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
154
|
-
| GitHub Copilot | `.github/copilot-instructions.md` | `/kgraph-scan` · `/kgraph-update` · `/kgraph-visualize` · `/kgraph-history` · `/kgraph-capture` |
|
|
155
|
-
| Codex | `AGENTS.md` | `.agents/skills/kgraph/SKILL.md` (VS Code Agent Skills standard) |
|
|
156
|
-
| Cursor | `.cursor/rules/kgraph.mdc` | Built into the rule |
|
|
157
|
-
| Claude Code | `CLAUDE.md` | `/kgraph` · `/kgraph-scan` · `/kgraph-update` · `/kgraph-visualize` · `/kgraph-history` |
|
|
150
|
+
Return context from existing maps and cognition without scanning or updating first.
|
|
158
151
|
|
|
159
|
-
|
|
152
|
+
```bash
|
|
153
|
+
kgraph update
|
|
154
|
+
kgraph update --dry-run
|
|
155
|
+
```
|
|
160
156
|
|
|
161
|
-
|
|
157
|
+
Process Markdown notes from `.kgraph/inbox/` into durable cognition records.
|
|
162
158
|
|
|
163
|
-
|
|
159
|
+
```bash
|
|
160
|
+
kgraph visualize
|
|
161
|
+
kgraph visualize --port 3000
|
|
162
|
+
kgraph visualize --no-open
|
|
163
|
+
```
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
Open the local interactive dependency graph at `http://localhost:4242`.
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
|
-
kgraph
|
|
169
|
-
kgraph
|
|
168
|
+
kgraph history
|
|
169
|
+
kgraph history --last 10
|
|
170
|
+
kgraph history --json
|
|
171
|
+
```
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
kgraph context "auth token refresh" # get compact context for a topic
|
|
173
|
-
kgraph context "auth token refresh" --json # machine-readable output
|
|
174
|
-
kgraph update # process inbox notes into cognition
|
|
173
|
+
Show processed cognition sessions.
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
kgraph integrate add codex copilot cursor # add integrations
|
|
178
|
-
kgraph integrate remove cursor # remove an integration
|
|
175
|
+
## AI Tool Integrations
|
|
179
176
|
|
|
180
|
-
|
|
181
|
-
kgraph visualize --port 3000 # custom port
|
|
182
|
-
kgraph visualize --no-open # print URL, don't open browser
|
|
177
|
+
KGraph integrations are local files. They do not start background agents, call AI providers, or send data anywhere.
|
|
183
178
|
|
|
184
|
-
|
|
185
|
-
kgraph
|
|
186
|
-
kgraph
|
|
179
|
+
```bash
|
|
180
|
+
kgraph integrate add codex copilot cursor claude-code
|
|
181
|
+
kgraph integrate list
|
|
182
|
+
kgraph integrate remove cursor
|
|
187
183
|
```
|
|
188
184
|
|
|
189
|
-
|
|
185
|
+
| Tool | Files KGraph manages |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| Codex | `AGENTS.md`, `.agents/skills/kgraph/SKILL.md` |
|
|
188
|
+
| GitHub Copilot | `.github/copilot-instructions.md`, `.github/prompts/*` |
|
|
189
|
+
| Cursor | `.cursor/rules/kgraph.mdc` |
|
|
190
|
+
| Claude Code | `CLAUDE.md`, `.claude/commands/*` |
|
|
191
|
+
|
|
192
|
+
KGraph preserves existing user-authored content and updates only its marked instruction blocks or generated command files.
|
|
193
|
+
|
|
194
|
+
## What Gets Stored
|
|
195
|
+
|
|
196
|
+
All runtime data lives under `.kgraph/`:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
.kgraph/
|
|
200
|
+
├── config.yaml
|
|
201
|
+
├── map/
|
|
202
|
+
│ ├── files.json
|
|
203
|
+
│ ├── symbols.json
|
|
204
|
+
│ ├── dependencies.json
|
|
205
|
+
│ └── relationships.json
|
|
206
|
+
├── inbox/
|
|
207
|
+
├── cognition/
|
|
208
|
+
├── domains/
|
|
209
|
+
├── interactions/processed/
|
|
210
|
+
└── context/
|
|
211
|
+
```
|
|
190
212
|
|
|
191
|
-
|
|
213
|
+
The files are local, inspectable, and human-readable. There is no database, telemetry, cloud service, account, API key, embedding service, or model provider.
|
|
192
214
|
|
|
193
|
-
|
|
194
|
-
| ----------------- | ---------------------------------------------------------------------- |
|
|
195
|
-
| **File map** | every source file, language, size |
|
|
196
|
-
| **Symbol index** | functions, classes, methods, exports per file |
|
|
197
|
-
| **Import graph** | which files import which, dependency chains |
|
|
198
|
-
| **Relationships** | call sites, re-exports, shared types |
|
|
199
|
-
| **Cognition** | past decisions, architectural constraints, debugging insights, gotchas |
|
|
215
|
+
## Language Support
|
|
200
216
|
|
|
201
|
-
|
|
217
|
+
KGraph deeply scans:
|
|
202
218
|
|
|
203
|
-
|
|
219
|
+
- TypeScript and JavaScript
|
|
220
|
+
- Python
|
|
221
|
+
- Go
|
|
222
|
+
- Rust
|
|
223
|
+
- Java and Kotlin
|
|
224
|
+
- C and C++
|
|
225
|
+
- C#
|
|
204
226
|
|
|
205
|
-
|
|
227
|
+
Other common file types still appear in the file map with generic metadata, so context queries can still point to docs, config, SQL, CSS, HTML, YAML, and similar files.
|
|
206
228
|
|
|
207
|
-
##
|
|
229
|
+
## Visualization
|
|
208
230
|
|
|
209
|
-
|
|
231
|
+
```bash
|
|
232
|
+
kgraph visualize
|
|
233
|
+
```
|
|
210
234
|
|
|
211
|
-
|
|
212
|
-
- No embeddings or vector databases
|
|
213
|
-
- No cloud services or telemetry
|
|
214
|
-
- No background daemons
|
|
215
|
-
- No model provider
|
|
235
|
+
The graph shows files, symbols, imports, cognition notes, and relationship edges. Cognition notes are colored by reference health:
|
|
216
236
|
|
|
217
|
-
|
|
237
|
+
- current
|
|
238
|
+
- mixed
|
|
239
|
+
- stale
|
|
240
|
+
- unresolved
|
|
218
241
|
|
|
219
|
-
|
|
242
|
+
Use it when you want to inspect what KGraph currently knows, find stale notes after refactors, or export a graph image for a report.
|
|
220
243
|
|
|
221
244
|
## Development
|
|
222
245
|
|
|
@@ -226,60 +249,44 @@ npm run build
|
|
|
226
249
|
npm test
|
|
227
250
|
```
|
|
228
251
|
|
|
229
|
-
|
|
252
|
+
Run the local TypeScript CLI without installing globally:
|
|
230
253
|
|
|
231
254
|
```bash
|
|
232
|
-
npm run kgraph -- init
|
|
233
|
-
npm run kgraph --
|
|
255
|
+
npm run kgraph -- init
|
|
256
|
+
npm run kgraph -- "auth token refresh"
|
|
257
|
+
npm run kgraph -- doctor
|
|
234
258
|
```
|
|
235
259
|
|
|
236
|
-
|
|
260
|
+
Package checks:
|
|
237
261
|
|
|
238
262
|
```bash
|
|
239
|
-
npm
|
|
240
|
-
|
|
241
|
-
kgraph init --integrations codex,copilot
|
|
263
|
+
npm run pack:dry
|
|
264
|
+
npm run release:pack
|
|
242
265
|
```
|
|
243
266
|
|
|
244
|
-
---
|
|
245
|
-
|
|
246
267
|
## Release
|
|
247
268
|
|
|
248
|
-
Releases are tag-driven
|
|
269
|
+
Releases are tag-driven:
|
|
249
270
|
|
|
250
271
|
```bash
|
|
251
272
|
npm version patch
|
|
252
273
|
git push origin main --follow-tags
|
|
253
274
|
```
|
|
254
275
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
---
|
|
258
|
-
|
|
259
|
-
## Visualization
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
kgraph visualize
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Starts a local server at `http://localhost:4242` and opens an interactive dependency graph in your browser. No install required — two CDN scripts (Cytoscape.js + dagre layout) are loaded at view time.
|
|
266
|
-
|
|
267
|
-
**What you see:**
|
|
276
|
+
The release workflow builds, tests, packs, publishes the npm package on version tags, creates a GitHub Release, and uploads the tarball artifact.
|
|
268
277
|
|
|
269
|
-
|
|
270
|
-
- Cognition notes as diamonds, colored by health (green = current, amber = mixed, red = stale)
|
|
271
|
-
- Import edges showing real dependency flow
|
|
272
|
-
- Dashed blue edges linking cognition notes to the files they describe
|
|
273
|
-
- Click any node for a metadata panel (path, size, domain, related symbols)
|
|
274
|
-
- Toggle cognition overlay on/off
|
|
275
|
-
- Switch layout: Hierarchical (default), Force-directed, Grid, Concentric
|
|
276
|
-
- **Export PNG** — 2× resolution, dark background, ready for reports or slides
|
|
278
|
+
## Design Principles
|
|
277
279
|
|
|
278
|
-
|
|
280
|
+
- Local-first: the repo intelligence stays in your repo.
|
|
281
|
+
- Explicit: no daemon and no hidden background process.
|
|
282
|
+
- Inspectable: generated knowledge is JSON, YAML, and Markdown.
|
|
283
|
+
- Deterministic first: useful ranking without requiring embeddings or a model.
|
|
284
|
+
- Assistant-friendly: one normal command, with lower-level commands available when needed.
|
|
279
285
|
|
|
280
286
|
## Roadmap
|
|
281
287
|
|
|
282
|
-
-
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
-
|
|
288
|
+
- Smarter cross-file symbol and call relationship inference.
|
|
289
|
+
- Stronger TypeScript path alias and package export resolution.
|
|
290
|
+
- Richer graph filtering for large repositories.
|
|
291
|
+
- Optional MCP server for editor tool-call access.
|
|
292
|
+
- Team workflows for shared committed cognition.
|
|
@@ -19,10 +19,10 @@ export function registerContextCommand(program) {
|
|
|
19
19
|
const config = await loadConfig(workspace);
|
|
20
20
|
const maps = await readMaps(workspace);
|
|
21
21
|
const response = await queryContext(workspace, config, maps, query);
|
|
22
|
-
console.log(options.json ? JSON.stringify(response, null, 2) :
|
|
22
|
+
console.log(options.json ? JSON.stringify(response, null, 2) : renderContextMarkdown(response));
|
|
23
23
|
}));
|
|
24
24
|
}
|
|
25
|
-
function
|
|
25
|
+
export function renderContextMarkdown(response) {
|
|
26
26
|
const lines = [`# KGraph Context`, ``, `Query: ${response.query}`, ``];
|
|
27
27
|
lines.push("## Matched Domains", "");
|
|
28
28
|
lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} (${item.reasons.join(", ")})`)));
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig } from '../../config/config.js';
|
|
4
|
+
import { listIntegrations } from '../../integrations/integration-store.js';
|
|
5
|
+
import { assertWorkspace, pathExists, resolveWorkspace, } from '../../storage/kgraph-paths.js';
|
|
6
|
+
import { mapPaths, mapsExist, readMaps } from '../../storage/map-store.js';
|
|
7
|
+
import { runCommand } from '../errors.js';
|
|
8
|
+
export function registerDoctorCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('doctor')
|
|
11
|
+
.description('Check KGraph workspace health and next actions')
|
|
12
|
+
.action(() => runCommand(async () => {
|
|
13
|
+
const rootPath = process.cwd();
|
|
14
|
+
const workspace = resolveWorkspace(rootPath);
|
|
15
|
+
const checks = [];
|
|
16
|
+
const initialized = await pathExists(workspace.kgraphPath);
|
|
17
|
+
checks.push({
|
|
18
|
+
label: 'workspace',
|
|
19
|
+
ok: initialized,
|
|
20
|
+
detail: initialized ? '.kgraph exists' : 'run `kgraph init` first',
|
|
21
|
+
});
|
|
22
|
+
if (!initialized) {
|
|
23
|
+
printChecks(checks);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await assertWorkspace(rootPath);
|
|
28
|
+
const config = await loadConfig(workspace);
|
|
29
|
+
checks.push({
|
|
30
|
+
label: 'config',
|
|
31
|
+
ok: true,
|
|
32
|
+
detail: `${config.include.length} include pattern(s), ${config.exclude.length} exclude pattern(s)`,
|
|
33
|
+
});
|
|
34
|
+
const mapStatus = await mapsExist(workspace);
|
|
35
|
+
checks.push({
|
|
36
|
+
label: 'maps',
|
|
37
|
+
ok: mapStatus,
|
|
38
|
+
detail: mapStatus ? 'structural maps are present' : 'run `kgraph scan` or just `kgraph`',
|
|
39
|
+
});
|
|
40
|
+
if (mapStatus) {
|
|
41
|
+
const maps = await readMaps(workspace);
|
|
42
|
+
checks.push({
|
|
43
|
+
label: 'scan result',
|
|
44
|
+
ok: true,
|
|
45
|
+
detail: `${maps.fileMap.files.length} files, ${maps.symbolMap.symbols.length} symbols, ${maps.dependencyMap.dependencies.length} dependencies`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const paths = mapPaths(workspace);
|
|
50
|
+
const missing = [];
|
|
51
|
+
for (const [name, filePath] of Object.entries(paths)) {
|
|
52
|
+
if (!(await pathExists(filePath)))
|
|
53
|
+
missing.push(name);
|
|
54
|
+
}
|
|
55
|
+
checks.push({
|
|
56
|
+
label: 'missing maps',
|
|
57
|
+
ok: false,
|
|
58
|
+
detail: missing.join(', '),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const inboxCount = await countMarkdownFiles(workspace.inboxPath);
|
|
62
|
+
checks.push({
|
|
63
|
+
label: 'inbox',
|
|
64
|
+
ok: true,
|
|
65
|
+
detail: inboxCount === 0
|
|
66
|
+
? 'no pending cognition notes'
|
|
67
|
+
: `${inboxCount} note(s) waiting for \`kgraph update\``,
|
|
68
|
+
});
|
|
69
|
+
const integrations = await listIntegrations(workspace);
|
|
70
|
+
checks.push({
|
|
71
|
+
label: 'integrations',
|
|
72
|
+
ok: integrations.every((integration) => integration.targetExists),
|
|
73
|
+
detail: integrations.length === 0
|
|
74
|
+
? 'none configured'
|
|
75
|
+
: integrations
|
|
76
|
+
.map((integration) => integration.targetExists
|
|
77
|
+
? `${integration.name}: ${integration.targetPath}`
|
|
78
|
+
: `${integration.name}: missing ${integration.targetPath}`)
|
|
79
|
+
.join('; '),
|
|
80
|
+
});
|
|
81
|
+
printChecks(checks);
|
|
82
|
+
if (checks.some((check) => !check.ok)) {
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
async function countMarkdownFiles(dirPath) {
|
|
88
|
+
if (!(await pathExists(dirPath))) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
92
|
+
return entries.filter((entry) => entry.isFile() && path.extname(entry.name) === '.md').length;
|
|
93
|
+
}
|
|
94
|
+
function printChecks(checks) {
|
|
95
|
+
console.log('KGraph Doctor');
|
|
96
|
+
console.log('');
|
|
97
|
+
for (const check of checks) {
|
|
98
|
+
console.log(`${check.ok ? 'OK' : 'FAIL'} ${check.label}: ${check.detail}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDefaultWorkflow(query?: string): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { updateCognition } from '../../cognition/cognition-updater.js';
|
|
2
|
+
import { refreshCognitionReferenceStatuses } from '../../cognition/cognition-updater.js';
|
|
3
|
+
import { loadConfig } from '../../config/config.js';
|
|
4
|
+
import { queryContext } from '../../context/context-query.js';
|
|
5
|
+
import { scanRepository } from '../../scanner/repo-scanner.js';
|
|
6
|
+
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
7
|
+
import { readMaps, writeMaps } from '../../storage/map-store.js';
|
|
8
|
+
import { runCommand } from '../errors.js';
|
|
9
|
+
import { renderContextMarkdown } from './context.js';
|
|
10
|
+
export async function runDefaultWorkflow(query) {
|
|
11
|
+
await runCommand(async () => {
|
|
12
|
+
const topic = query?.trim();
|
|
13
|
+
const workspace = await assertWorkspace(process.cwd());
|
|
14
|
+
const config = await loadConfig(workspace);
|
|
15
|
+
const previousMaps = await readMaps(workspace);
|
|
16
|
+
const scan = await scanRepository(workspace.rootPath, config, {
|
|
17
|
+
files: previousMaps.fileMap.files,
|
|
18
|
+
symbols: previousMaps.symbolMap.symbols,
|
|
19
|
+
dependencies: previousMaps.dependencyMap.dependencies,
|
|
20
|
+
relationships: previousMaps.relationshipMap.relationships,
|
|
21
|
+
warnings: [],
|
|
22
|
+
});
|
|
23
|
+
await writeMaps(workspace, scan);
|
|
24
|
+
await refreshCognitionReferenceStatuses(workspace, {
|
|
25
|
+
files: scan.files,
|
|
26
|
+
symbols: scan.symbols,
|
|
27
|
+
});
|
|
28
|
+
const update = await updateCognition(workspace, { files: scan.files, symbols: scan.symbols }, false);
|
|
29
|
+
console.log(`KGraph refreshed ${scan.files.length} files, ${scan.symbols.length} symbols, and ${update.processed.length} cognition notes.`);
|
|
30
|
+
for (const warning of [...scan.warnings, ...update.warnings]) {
|
|
31
|
+
console.warn(`Warning: ${warning}`);
|
|
32
|
+
}
|
|
33
|
+
if (!topic) {
|
|
34
|
+
console.log('Add a topic to return compact context, for example: kgraph "auth token refresh"');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const maps = await readMaps(workspace);
|
|
38
|
+
const response = await queryContext(workspace, config, maps, topic);
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(renderContextMarkdown(response));
|
|
41
|
+
});
|
|
42
|
+
}
|
package/dist/cli/help.js
CHANGED
|
@@ -14,16 +14,22 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
14
14
|
` ${theme.hex('#c084fc')('and Claude Code reuse repo structure, decisions, and debugging history.')}`,
|
|
15
15
|
'',
|
|
16
16
|
theme.bold('Usage'),
|
|
17
|
+
' kgraph [topic]',
|
|
17
18
|
' kgraph <command> [options]',
|
|
18
19
|
'',
|
|
19
20
|
theme.bold('Start'),
|
|
20
|
-
command('init', '
|
|
21
|
+
command('init', 'Required once: create .kgraph/ workspace'),
|
|
21
22
|
command('init --integrations codex,cursor', 'Initialize and connect AI tools'),
|
|
22
23
|
'',
|
|
24
|
+
theme.bold('Daily workflow'),
|
|
25
|
+
command('kgraph', 'Refresh scan maps and process pending cognition notes'),
|
|
26
|
+
command('kgraph "auth token refresh"', 'Refresh everything and return compact context for a topic'),
|
|
27
|
+
'',
|
|
23
28
|
theme.bold('Workflows'),
|
|
24
|
-
command('scan', '
|
|
25
|
-
command('context "auth token refresh"', '
|
|
26
|
-
command('update', '
|
|
29
|
+
command('scan', 'Optional: refresh only file, symbol, import, and relationship maps'),
|
|
30
|
+
command('context "auth token refresh"', 'Optional: return context without scanning or updating'),
|
|
31
|
+
command('update', 'Optional: process only .kgraph/inbox Markdown cognition notes'),
|
|
32
|
+
command('doctor', 'Check workspace health and next actions'),
|
|
27
33
|
command('visualize', 'Interactive dependency graph at http://localhost:4242'),
|
|
28
34
|
command('history', 'Timeline of processed cognition sessions'),
|
|
29
35
|
'',
|
|
@@ -38,8 +44,8 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
38
44
|
'',
|
|
39
45
|
`${theme.yellow('Examples')}`,
|
|
40
46
|
' kgraph init --integrations codex,copilot,cursor',
|
|
41
|
-
' kgraph
|
|
42
|
-
' kgraph
|
|
47
|
+
' kgraph "blog admin token usage"',
|
|
48
|
+
' kgraph doctor',
|
|
43
49
|
'',
|
|
44
50
|
theme.dim('Docs: https://github.com/kentwynn/KGraph#readme'),
|
|
45
51
|
'',
|
package/dist/cli/index.js
CHANGED
|
@@ -4,12 +4,14 @@ import { realpathSync } from 'node:fs';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { registerContextCommand } from './commands/context.js';
|
|
7
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
7
8
|
import { registerHistoryCommand } from './commands/history.js';
|
|
8
9
|
import { registerInitCommand } from './commands/init.js';
|
|
9
10
|
import { registerIntegrateCommand } from './commands/integrate.js';
|
|
10
11
|
import { registerScanCommand } from './commands/scan.js';
|
|
11
12
|
import { registerUpdateCommand } from './commands/update.js';
|
|
12
13
|
import { registerVisualizeCommand } from './commands/visualize.js';
|
|
14
|
+
import { runDefaultWorkflow } from './commands/workflow.js';
|
|
13
15
|
import { renderRootHelp } from './help.js';
|
|
14
16
|
const require = createRequire(import.meta.url);
|
|
15
17
|
const { version } = require('../../package.json');
|
|
@@ -18,9 +20,13 @@ export function createProgram() {
|
|
|
18
20
|
program
|
|
19
21
|
.name('kgraph')
|
|
20
22
|
.description('Persistent repo intelligence for AI coding assistants')
|
|
23
|
+
.argument('[topic...]', 'Run the default refresh workflow and optionally return context for a topic')
|
|
21
24
|
.version(version)
|
|
22
25
|
.addHelpText('beforeAll', renderRootHelp())
|
|
23
|
-
.helpOption(false)
|
|
26
|
+
.helpOption(false)
|
|
27
|
+
.action(async (topicParts = []) => {
|
|
28
|
+
await runDefaultWorkflow(topicParts.join(' '));
|
|
29
|
+
});
|
|
24
30
|
program.option('-h, --help', 'Show this help');
|
|
25
31
|
program.hook('preAction', (thisCommand) => {
|
|
26
32
|
if (thisCommand.opts().help) {
|
|
@@ -35,13 +41,12 @@ export function createProgram() {
|
|
|
35
41
|
registerIntegrateCommand(program);
|
|
36
42
|
registerVisualizeCommand(program);
|
|
37
43
|
registerHistoryCommand(program);
|
|
44
|
+
registerDoctorCommand(program);
|
|
38
45
|
return program;
|
|
39
46
|
}
|
|
40
47
|
if (isCliEntrypoint()) {
|
|
41
48
|
const program = createProgram();
|
|
42
|
-
if (process.argv.
|
|
43
|
-
process.argv.includes('-h') ||
|
|
44
|
-
process.argv.includes('--help')) {
|
|
49
|
+
if (process.argv.includes('-h') || process.argv.includes('--help')) {
|
|
45
50
|
console.log(renderRootHelp());
|
|
46
51
|
}
|
|
47
52
|
else {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type { ScanResult } from
|
|
1
|
+
import type { CognitionNote, ReferenceStatus } from '../types/cognition.js';
|
|
2
|
+
import type { KGraphWorkspace } from '../types/config.js';
|
|
3
|
+
import type { ScanResult } from '../types/maps.js';
|
|
4
4
|
export interface UpdateResult {
|
|
5
5
|
processed: CognitionNote[];
|
|
6
6
|
warnings: string[];
|
|
7
7
|
}
|
|
8
|
-
export declare function updateCognition(workspace: KGraphWorkspace, currentMaps: Pick<ScanResult,
|
|
9
|
-
export declare function refreshCognitionReferenceStatuses(workspace: KGraphWorkspace, currentMaps: Pick<ScanResult,
|
|
10
|
-
export declare function evaluateReferenceStatus(relatedFiles: string[], relatedSymbols: string[], currentMaps: Pick<ScanResult,
|
|
8
|
+
export declare function updateCognition(workspace: KGraphWorkspace, currentMaps: Pick<ScanResult, 'files' | 'symbols'>, dryRun?: boolean): Promise<UpdateResult>;
|
|
9
|
+
export declare function refreshCognitionReferenceStatuses(workspace: KGraphWorkspace, currentMaps: Pick<ScanResult, 'files' | 'symbols'>): Promise<void>;
|
|
10
|
+
export declare function evaluateReferenceStatus(relatedFiles: string[], relatedSymbols: string[], currentMaps: Pick<ScanResult, 'files' | 'symbols'>): ReferenceStatus;
|
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import path from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { archiveInboxNote, listInboxNotes, readCognitionNotes, slugify, writeCognitionNote, writeDomainRecord, } from '../storage/cognition-store.js';
|
|
4
|
+
import { parseMarkdownNote } from './markdown-note-parser.js';
|
|
5
5
|
export async function updateCognition(workspace, currentMaps, dryRun = false) {
|
|
6
6
|
const inboxNotes = await listInboxNotes(workspace);
|
|
7
7
|
const processed = [];
|
|
8
8
|
const warnings = [];
|
|
9
9
|
for (const inboxPath of inboxNotes) {
|
|
10
10
|
try {
|
|
11
|
-
const raw = await readFile(inboxPath,
|
|
11
|
+
const raw = await readFile(inboxPath, 'utf8');
|
|
12
12
|
const parsed = parseMarkdownNote(raw);
|
|
13
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g,
|
|
14
|
-
const id = `${timestamp}-${slugify(parsed.title) || path.basename(inboxPath,
|
|
13
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
14
|
+
const id = `${timestamp}-${slugify(parsed.title) || path.basename(inboxPath, '.md')}`;
|
|
15
15
|
const archivedPath = path.join(workspace.processedInteractionsPath, `${timestamp}-${path.basename(inboxPath)}`);
|
|
16
16
|
const note = {
|
|
17
17
|
...parsed,
|
|
18
18
|
id,
|
|
19
|
-
sourceInboxPath: path
|
|
20
|
-
|
|
19
|
+
sourceInboxPath: path
|
|
20
|
+
.relative(workspace.rootPath, inboxPath)
|
|
21
|
+
.split(path.sep)
|
|
22
|
+
.join('/'),
|
|
23
|
+
processedPath: path
|
|
24
|
+
.relative(workspace.rootPath, archivedPath)
|
|
25
|
+
.split(path.sep)
|
|
26
|
+
.join('/'),
|
|
21
27
|
createdAt: new Date().toISOString(),
|
|
22
|
-
referencesStatus: evaluateReferenceStatus(parsed.relatedFiles, parsed.relatedSymbols, currentMaps)
|
|
28
|
+
referencesStatus: evaluateReferenceStatus(parsed.relatedFiles, parsed.relatedSymbols, currentMaps),
|
|
23
29
|
};
|
|
24
30
|
processed.push(note);
|
|
25
31
|
warnings.push(...parsed.warnings.map((warning) => `${path.basename(inboxPath)}: ${warning}`));
|
|
@@ -38,9 +44,28 @@ export async function updateCognition(workspace, currentMaps, dryRun = false) {
|
|
|
38
44
|
export async function refreshCognitionReferenceStatuses(workspace, currentMaps) {
|
|
39
45
|
const notes = await readCognitionNotes(workspace);
|
|
40
46
|
for (const note of notes) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// Re-parse relatedSymbols from the archived raw markdown if available.
|
|
48
|
+
// This migrates old notes that were parsed with the old plain-text heuristic
|
|
49
|
+
// (which produced false positives like JWT, CSRF, TODO) to the current
|
|
50
|
+
// backtick-only logic.
|
|
51
|
+
let relatedSymbols = note.relatedSymbols;
|
|
52
|
+
if (note.processedPath) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = await readFile(path.join(workspace.rootPath, note.processedPath), 'utf8');
|
|
55
|
+
relatedSymbols = parseMarkdownNote(raw).relatedSymbols;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// archived file missing — keep stored symbols
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const nextStatus = evaluateReferenceStatus(note.relatedFiles, relatedSymbols, currentMaps);
|
|
62
|
+
if (nextStatus !== note.referencesStatus ||
|
|
63
|
+
relatedSymbols !== note.relatedSymbols) {
|
|
64
|
+
await writeCognitionNote(workspace, {
|
|
65
|
+
...note,
|
|
66
|
+
relatedSymbols,
|
|
67
|
+
referencesStatus: nextStatus,
|
|
68
|
+
});
|
|
44
69
|
}
|
|
45
70
|
}
|
|
46
71
|
}
|
|
@@ -49,21 +74,21 @@ export function evaluateReferenceStatus(relatedFiles, relatedSymbols, currentMap
|
|
|
49
74
|
const symbolNames = new Set(currentMaps.symbols.map((symbol) => symbol.name));
|
|
50
75
|
const references = [
|
|
51
76
|
...relatedFiles.map((file) => filePaths.has(file)),
|
|
52
|
-
...relatedSymbols.map((symbol) => symbolNames.has(symbol))
|
|
77
|
+
...relatedSymbols.map((symbol) => symbolNames.has(symbol)),
|
|
53
78
|
];
|
|
54
79
|
if (references.length === 0) {
|
|
55
|
-
return
|
|
80
|
+
return 'unresolved';
|
|
56
81
|
}
|
|
57
82
|
if (references.every(Boolean)) {
|
|
58
|
-
return
|
|
83
|
+
return 'current';
|
|
59
84
|
}
|
|
60
85
|
if (references.every((value) => !value)) {
|
|
61
|
-
return
|
|
86
|
+
return 'stale';
|
|
62
87
|
}
|
|
63
|
-
return
|
|
88
|
+
return 'mixed';
|
|
64
89
|
}
|
|
65
90
|
function toDomainRecord(note, currentMaps) {
|
|
66
|
-
const name = note.domain ??
|
|
91
|
+
const name = note.domain ?? 'general';
|
|
67
92
|
const fileSet = new Set(currentMaps.files.map((file) => file.path));
|
|
68
93
|
const symbolSet = new Set(currentMaps.symbols.map((symbol) => symbol.name));
|
|
69
94
|
return {
|
|
@@ -72,6 +97,6 @@ function toDomainRecord(note, currentMaps) {
|
|
|
72
97
|
tags: note.tags,
|
|
73
98
|
files: note.relatedFiles.filter((file) => fileSet.has(file)),
|
|
74
99
|
symbols: note.relatedSymbols.filter((symbol) => symbolSet.has(symbol)),
|
|
75
|
-
cognitionNotes: [note.id]
|
|
100
|
+
cognitionNotes: [note.id],
|
|
76
101
|
};
|
|
77
102
|
}
|
|
@@ -14,7 +14,7 @@ export function parseMarkdownNote(markdown) {
|
|
|
14
14
|
summary: sections.Summary,
|
|
15
15
|
sections,
|
|
16
16
|
relatedFiles: unique(extractMatches(combined, PATH_REF)),
|
|
17
|
-
relatedSymbols: unique(extractSymbolRefs(
|
|
17
|
+
relatedSymbols: unique(extractSymbolRefs(sections)),
|
|
18
18
|
warnings,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
@@ -59,23 +59,12 @@ function parseSections(body) {
|
|
|
59
59
|
function extractMatches(text, regex) {
|
|
60
60
|
return [...text.matchAll(regex)].map((match) => match[1]);
|
|
61
61
|
}
|
|
62
|
-
function extractSymbolRefs(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'Decisions',
|
|
69
|
-
'Debugging',
|
|
70
|
-
'Conclusions',
|
|
71
|
-
]);
|
|
72
|
-
// Backtick-quoted: accept any identifier in backticks
|
|
73
|
-
const backtickSymbols = [...text.matchAll(/`([A-Za-z_$][\w$]{2,})`/g)].map((m) => m[1]);
|
|
74
|
-
// Plain text: only camelCase, PascalCase, ALL_CAPS, or snake_case (must contain uppercase or underscore after first char)
|
|
75
|
-
const plainSymbols = [...text.matchAll(/\b([A-Za-z_$][\w$]{2,})\b/g)]
|
|
76
|
-
.map((m) => m[1])
|
|
77
|
-
.filter((item) => /[A-Z_]/.test(item.slice(1)));
|
|
78
|
-
return unique([...backtickSymbols, ...plainSymbols].filter((item) => !stopwords.has(item) && !item.includes('.')));
|
|
62
|
+
function extractSymbolRefs(sections) {
|
|
63
|
+
// Prefer declared symbols in the Key Symbols section; fall back to backtick
|
|
64
|
+
// items across all sections. Never use plain-text heuristics — they produce
|
|
65
|
+
// false positives for domain vocabulary like JWT, CSRF, TODO, Next, etc.
|
|
66
|
+
const text = sections['Key Symbols'] ?? Object.values(sections).join('\n');
|
|
67
|
+
return [...text.matchAll(/`([A-Za-z_$][\w$]{2,})`/g)].map((m) => m[1]);
|
|
79
68
|
}
|
|
80
69
|
function unique(items) {
|
|
81
70
|
return [...new Set(items)];
|
|
@@ -26,6 +26,31 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
26
26
|
{ name: 'tags', value: (domain) => domain.tags },
|
|
27
27
|
{ name: 'path', value: (domain) => domain.pathHints },
|
|
28
28
|
]).slice(0, max);
|
|
29
|
+
const relatedIds = new Set([
|
|
30
|
+
...relevantFiles.map((file) => file.item.path),
|
|
31
|
+
...relevantSymbols.map((symbol) => symbol.item.id),
|
|
32
|
+
...relevantSymbols.map((symbol) => symbol.item.filePath),
|
|
33
|
+
...relevantCognition.flatMap((note) => [
|
|
34
|
+
...note.item.relatedFiles,
|
|
35
|
+
...note.item.relatedSymbols,
|
|
36
|
+
]),
|
|
37
|
+
...matchedDomains.flatMap((domain) => [
|
|
38
|
+
...domain.item.files,
|
|
39
|
+
...domain.item.symbols,
|
|
40
|
+
]),
|
|
41
|
+
]);
|
|
42
|
+
const rankedRelationships = rankByFields(query, maps.relationshipMap.relationships, [
|
|
43
|
+
{ name: 'source', value: (relationship) => relationship.sourceId },
|
|
44
|
+
{ name: 'target', value: (relationship) => relationship.targetId },
|
|
45
|
+
{ name: 'type', value: (relationship) => relationship.relationshipType },
|
|
46
|
+
]);
|
|
47
|
+
const relationships = [
|
|
48
|
+
...maps.relationshipMap.relationships.filter((relationship) => relatedIds.has(relationship.sourceId) ||
|
|
49
|
+
relatedIds.has(relationship.targetId)),
|
|
50
|
+
...rankedRelationships.map((relationship) => relationship.item),
|
|
51
|
+
].filter((relationship, index, all) => all.findIndex((candidate) => candidate.sourceId === relationship.sourceId &&
|
|
52
|
+
candidate.targetId === relationship.targetId &&
|
|
53
|
+
candidate.relationshipType === relationship.relationshipType) === index);
|
|
29
54
|
const filePaths = new Set(maps.fileMap.files.map((f) => f.path));
|
|
30
55
|
const symbolNames = new Set(maps.symbolMap.symbols.map((s) => s.name));
|
|
31
56
|
const staleReferences = cognition
|
|
@@ -46,7 +71,7 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
46
71
|
relevantFiles,
|
|
47
72
|
relevantSymbols,
|
|
48
73
|
relevantCognition,
|
|
49
|
-
relationships:
|
|
74
|
+
relationships: relationships.slice(0, max),
|
|
50
75
|
staleReferences,
|
|
51
76
|
warnings: [],
|
|
52
77
|
};
|
|
@@ -80,7 +80,7 @@ export async function scanRepository(rootPath, config, previous) {
|
|
|
80
80
|
const extracted = extractSymbols(text, repoPath);
|
|
81
81
|
symbols.push(...extracted.symbols);
|
|
82
82
|
dependencies.push(...extracted.dependencies);
|
|
83
|
-
relationships.push(...extracted.relationships);
|
|
83
|
+
relationships.push(...extracted.relationships.filter((relationship) => relationship.relationshipType !== 'import'));
|
|
84
84
|
file.warnings.push(...extracted.warnings);
|
|
85
85
|
}
|
|
86
86
|
files.push(file);
|
|
@@ -100,9 +100,71 @@ export async function scanRepository(rootPath, config, previous) {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
resolveLocalDependencies(dependencies, files);
|
|
104
|
+
relationships.push(...buildImportRelationships(dependencies));
|
|
103
105
|
relationships.push(...detectMovedFiles(previous?.files ?? [], files));
|
|
104
106
|
return { files, symbols, dependencies, relationships, warnings };
|
|
105
107
|
}
|
|
108
|
+
const SOURCE_EXTENSIONS = [
|
|
109
|
+
'.ts',
|
|
110
|
+
'.tsx',
|
|
111
|
+
'.js',
|
|
112
|
+
'.jsx',
|
|
113
|
+
'.mjs',
|
|
114
|
+
'.cjs',
|
|
115
|
+
'.mts',
|
|
116
|
+
'.cts',
|
|
117
|
+
'.py',
|
|
118
|
+
'.go',
|
|
119
|
+
'.rs',
|
|
120
|
+
'.java',
|
|
121
|
+
'.kt',
|
|
122
|
+
'.kts',
|
|
123
|
+
'.c',
|
|
124
|
+
'.h',
|
|
125
|
+
'.cpp',
|
|
126
|
+
'.cc',
|
|
127
|
+
'.cxx',
|
|
128
|
+
'.hpp',
|
|
129
|
+
'.hxx',
|
|
130
|
+
'.cs',
|
|
131
|
+
];
|
|
132
|
+
function resolveLocalDependencies(dependencies, files) {
|
|
133
|
+
const filePaths = new Set(files.map((file) => file.path));
|
|
134
|
+
for (const dependency of dependencies) {
|
|
135
|
+
if (dependency.kind !== 'local') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
dependency.resolvedFile = resolveLocalDependencyPath(dependency.fromFile, dependency.specifier, filePaths);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function resolveLocalDependencyPath(fromFile, specifier, filePaths) {
|
|
142
|
+
if (!specifier.startsWith('.')) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), specifier));
|
|
146
|
+
const candidates = path.posix.extname(base)
|
|
147
|
+
? [base]
|
|
148
|
+
: [
|
|
149
|
+
...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
150
|
+
...SOURCE_EXTENSIONS.map((extension) => path.posix.join(base, `index${extension}`)),
|
|
151
|
+
];
|
|
152
|
+
return candidates.find((candidate) => filePaths.has(candidate));
|
|
153
|
+
}
|
|
154
|
+
function buildImportRelationships(dependencies) {
|
|
155
|
+
return dependencies.map((dependency) => ({
|
|
156
|
+
sourceType: 'file',
|
|
157
|
+
sourceId: dependency.fromFile,
|
|
158
|
+
targetType: dependency.kind === 'local' ? 'file' : 'package',
|
|
159
|
+
targetId: dependency.resolvedFile ?? dependency.specifier,
|
|
160
|
+
relationshipType: 'import',
|
|
161
|
+
confidence: dependency.resolvedFile
|
|
162
|
+
? 'high'
|
|
163
|
+
: dependency.kind === 'local'
|
|
164
|
+
? 'low'
|
|
165
|
+
: 'medium',
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
106
168
|
function detectMovedFiles(previousFiles, currentFiles) {
|
|
107
169
|
const currentPaths = new Set(currentFiles.map((file) => file.path));
|
|
108
170
|
const previousByHash = new Map(previousFiles
|
|
@@ -26,7 +26,11 @@ export async function writeCognitionNote(workspace, note) {
|
|
|
26
26
|
export async function writeDomainRecord(workspace, domain) {
|
|
27
27
|
await mkdir(workspace.domainsPath, { recursive: true });
|
|
28
28
|
const filePath = path.join(workspace.domainsPath, `${slugify(domain.name)}.md`);
|
|
29
|
-
await
|
|
29
|
+
const existing = (await pathExists(filePath))
|
|
30
|
+
? parseEmbeddedJson(await readFile(filePath, "utf8"))
|
|
31
|
+
: undefined;
|
|
32
|
+
const merged = existing ? mergeDomainRecords(existing, domain) : domain;
|
|
33
|
+
await writeFile(filePath, renderDomainRecord(merged), "utf8");
|
|
30
34
|
return filePath;
|
|
31
35
|
}
|
|
32
36
|
export async function readCognitionNotes(workspace) {
|
|
@@ -41,9 +45,9 @@ export async function readCognitionNotes(workspace) {
|
|
|
41
45
|
}
|
|
42
46
|
const filePath = path.join(workspace.cognitionPath, entry.name);
|
|
43
47
|
const raw = await readFile(filePath, "utf8");
|
|
44
|
-
const encoded = raw
|
|
48
|
+
const encoded = parseEmbeddedJson(raw);
|
|
45
49
|
if (encoded) {
|
|
46
|
-
notes.push(
|
|
50
|
+
notes.push(encoded);
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
return notes;
|
|
@@ -59,13 +63,35 @@ export async function readDomainRecords(workspace) {
|
|
|
59
63
|
continue;
|
|
60
64
|
}
|
|
61
65
|
const raw = await readFile(path.join(workspace.domainsPath, entry.name), "utf8");
|
|
62
|
-
const encoded = raw
|
|
66
|
+
const encoded = parseEmbeddedJson(raw);
|
|
63
67
|
if (encoded) {
|
|
64
|
-
domains.push(
|
|
68
|
+
domains.push(encoded);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
return domains;
|
|
68
72
|
}
|
|
73
|
+
function parseEmbeddedJson(raw) {
|
|
74
|
+
const encoded = raw.match(/```json\n([\s\S]*?)\n```/);
|
|
75
|
+
return encoded ? JSON.parse(encoded[1]) : undefined;
|
|
76
|
+
}
|
|
77
|
+
function mergeDomainRecords(existing, next) {
|
|
78
|
+
return {
|
|
79
|
+
...existing,
|
|
80
|
+
...next,
|
|
81
|
+
description: next.description ?? existing.description,
|
|
82
|
+
pathHints: unique([...existing.pathHints, ...next.pathHints]),
|
|
83
|
+
tags: unique([...existing.tags, ...next.tags]),
|
|
84
|
+
files: unique([...existing.files, ...next.files]),
|
|
85
|
+
symbols: unique([...existing.symbols, ...next.symbols]),
|
|
86
|
+
cognitionNotes: unique([
|
|
87
|
+
...existing.cognitionNotes,
|
|
88
|
+
...next.cognitionNotes,
|
|
89
|
+
]),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function unique(items) {
|
|
93
|
+
return [...new Set(items)];
|
|
94
|
+
}
|
|
69
95
|
export function slugify(value) {
|
|
70
96
|
return value
|
|
71
97
|
.trim()
|
|
@@ -13,4 +13,4 @@ export interface GraphData {
|
|
|
13
13
|
generatedAt: string;
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
export declare function buildGraph(fileMap: FileMap, symbolMap: SymbolMap, dependencyMap: DependencyMap,
|
|
16
|
+
export declare function buildGraph(fileMap: FileMap, symbolMap: SymbolMap, dependencyMap: DependencyMap, relationshipMap: RelationshipMap, cognitionNotes: CognitionNote[]): GraphData;
|
|
@@ -13,10 +13,19 @@ const STATUS_COLORS = {
|
|
|
13
13
|
stale: '#ef4444',
|
|
14
14
|
unresolved: '#6b7280',
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
const SYMBOL_COLORS = {
|
|
17
|
+
function: '#22c55e',
|
|
18
|
+
class: '#a855f7',
|
|
19
|
+
method: '#14b8a6',
|
|
20
|
+
export: '#f97316',
|
|
21
|
+
import: '#64748b',
|
|
22
|
+
};
|
|
23
|
+
export function buildGraph(fileMap, symbolMap, dependencyMap, relationshipMap, cognitionNotes) {
|
|
17
24
|
const elements = [];
|
|
18
25
|
const edgeIds = new Set();
|
|
26
|
+
const nodeIds = new Set();
|
|
19
27
|
for (const file of fileMap.files) {
|
|
28
|
+
nodeIds.add(file.id);
|
|
20
29
|
elements.push({
|
|
21
30
|
data: {
|
|
22
31
|
id: file.id,
|
|
@@ -31,6 +40,21 @@ export function buildGraph(fileMap, symbolMap, dependencyMap, _relationshipMap,
|
|
|
31
40
|
classes: `file ${file.language}`,
|
|
32
41
|
});
|
|
33
42
|
}
|
|
43
|
+
for (const symbol of symbolMap.symbols) {
|
|
44
|
+
nodeIds.add(symbol.id);
|
|
45
|
+
elements.push({
|
|
46
|
+
data: {
|
|
47
|
+
id: symbol.id,
|
|
48
|
+
label: symbol.name,
|
|
49
|
+
path: symbol.filePath,
|
|
50
|
+
kind: symbol.kind,
|
|
51
|
+
parentName: symbol.parentName ?? '',
|
|
52
|
+
type: 'symbol',
|
|
53
|
+
color: SYMBOL_COLORS[symbol.kind] ?? '#94a3b8',
|
|
54
|
+
},
|
|
55
|
+
classes: `symbol ${symbol.kind}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
34
58
|
for (const note of cognitionNotes) {
|
|
35
59
|
const id = `cognition-${note.id}`;
|
|
36
60
|
elements.push({
|
|
@@ -88,6 +112,27 @@ export function buildGraph(fileMap, symbolMap, dependencyMap, _relationshipMap,
|
|
|
88
112
|
}
|
|
89
113
|
}
|
|
90
114
|
}
|
|
115
|
+
for (const relationship of relationshipMap.relationships) {
|
|
116
|
+
if (!nodeIds.has(relationship.sourceId) || !nodeIds.has(relationship.targetId)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const edgeId = `rel-${relationship.relationshipType}-${relationship.sourceId}-${relationship.targetId}`;
|
|
120
|
+
if (edgeIds.has(edgeId)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
edgeIds.add(edgeId);
|
|
124
|
+
elements.push({
|
|
125
|
+
data: {
|
|
126
|
+
id: edgeId,
|
|
127
|
+
source: relationship.sourceId,
|
|
128
|
+
target: relationship.targetId,
|
|
129
|
+
type: relationship.relationshipType,
|
|
130
|
+
confidence: relationship.confidence,
|
|
131
|
+
label: relationship.relationshipType,
|
|
132
|
+
},
|
|
133
|
+
classes: `relationship ${relationship.relationshipType}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
91
136
|
return {
|
|
92
137
|
elements,
|
|
93
138
|
meta: {
|