purecontext-mcp 1.7.0 → 1.11.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/AGENT_INSTRUCTIONS.md +18 -10
- package/AGENT_REFERENCE.md +684 -561
- package/CHANGELOG.md +567 -445
- package/CODE-HISTORY.md +29 -1
- package/FULL-INSTALLATION-GUIDE.md +351 -341
- package/README.md +411 -339
- package/REFACTORING-SAFELY.md +338 -279
- package/SAFE-CHANGES.md +208 -156
- package/USER-GUIDE.md +3 -1
- package/WHY-PURECONTEXT.md +103 -73
- package/WORKFLOW-PR-REVIEW.md +245 -199
- package/dist/bin.d.ts +16 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +21 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +20 -15
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/install-writers.d.ts.map +1 -1
- package/dist/cli/install-writers.js +281 -36
- package/dist/cli/install-writers.js.map +1 -1
- package/dist/cli/resolve-node.d.ts +53 -0
- package/dist/cli/resolve-node.d.ts.map +1 -0
- package/dist/cli/resolve-node.js +84 -0
- package/dist/cli/resolve-node.js.map +1 -0
- package/dist/config/config-loader.js +24 -0
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config-schema.d.ts +71 -0
- package/dist/config/config-schema.d.ts.map +1 -1
- package/dist/config/config-schema.js +102 -0
- package/dist/config/config-schema.js.map +1 -1
- package/dist/core/db/api-keys.d.ts +1 -1
- package/dist/core/db/api-keys.d.ts.map +1 -1
- package/dist/core/db/api-keys.js +39 -39
- package/dist/core/db/api-keys.js.map +1 -1
- package/dist/core/db/co-change-store.d.ts +34 -0
- package/dist/core/db/co-change-store.d.ts.map +1 -0
- package/dist/core/db/co-change-store.js +78 -0
- package/dist/core/db/co-change-store.js.map +1 -0
- package/dist/core/db/schema.d.ts +3 -3
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +12 -30
- package/dist/core/db/schema.js.map +1 -1
- package/dist/core/db/sqlite-loader.d.ts +51 -0
- package/dist/core/db/sqlite-loader.d.ts.map +1 -0
- package/dist/core/db/sqlite-loader.js +94 -0
- package/dist/core/db/sqlite-loader.js.map +1 -0
- package/dist/core/db/wasm-sqlite.d.ts +4 -0
- package/dist/core/db/wasm-sqlite.d.ts.map +1 -0
- package/dist/core/db/wasm-sqlite.js +270 -0
- package/dist/core/db/wasm-sqlite.js.map +1 -0
- package/dist/core/diff-parser.d.ts.map +1 -1
- package/dist/core/diff-parser.js +6 -1
- package/dist/core/diff-parser.js.map +1 -1
- package/dist/core/git-log-reader.d.ts +28 -0
- package/dist/core/git-log-reader.d.ts.map +1 -1
- package/dist/core/git-log-reader.js +74 -3
- package/dist/core/git-log-reader.js.map +1 -1
- package/dist/core/index-manager.d.ts.map +1 -1
- package/dist/core/index-manager.js +20 -1
- package/dist/core/index-manager.js.map +1 -1
- package/dist/graph/path-resolver.js +86 -17
- package/dist/graph/path-resolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/node-guard.d.ts +15 -0
- package/dist/node-guard.d.ts.map +1 -0
- package/dist/node-guard.js +33 -0
- package/dist/node-guard.js.map +1 -0
- package/dist/server/admin-api.d.ts +1 -1
- package/dist/server/admin-api.d.ts.map +1 -1
- package/dist/server/admin-api.js +2 -2
- package/dist/server/admin-api.js.map +1 -1
- package/dist/server/auth/api-key.d.ts +1 -1
- package/dist/server/auth/api-key.d.ts.map +1 -1
- package/dist/server/mcp-server.d.ts.map +1 -1
- package/dist/server/mcp-server.js +25 -0
- package/dist/server/mcp-server.js.map +1 -1
- package/dist/server/tools/analyze-diff.d.ts +8 -0
- package/dist/server/tools/analyze-diff.d.ts.map +1 -1
- package/dist/server/tools/analyze-diff.js +80 -16
- package/dist/server/tools/analyze-diff.js.map +1 -1
- package/dist/server/tools/change-synthesis.d.ts +90 -0
- package/dist/server/tools/change-synthesis.d.ts.map +1 -0
- package/dist/server/tools/change-synthesis.js +236 -0
- package/dist/server/tools/change-synthesis.js.map +1 -0
- package/dist/server/tools/co-change.d.ts +65 -0
- package/dist/server/tools/co-change.d.ts.map +1 -0
- package/dist/server/tools/co-change.js +146 -0
- package/dist/server/tools/co-change.js.map +1 -0
- package/dist/server/tools/compare-change-impact.d.ts +58 -0
- package/dist/server/tools/compare-change-impact.d.ts.map +1 -0
- package/dist/server/tools/compare-change-impact.js +0 -0
- package/dist/server/tools/compare-change-impact.js.map +1 -0
- package/dist/server/tools/find-refactoring-opportunities.d.ts +1 -1
- package/dist/server/tools/get-architecture-snapshot.d.ts.map +1 -1
- package/dist/server/tools/get-architecture-snapshot.js +28 -14
- package/dist/server/tools/get-architecture-snapshot.js.map +1 -1
- package/dist/server/tools/get-churn-metrics.d.ts.map +1 -1
- package/dist/server/tools/get-churn-metrics.js +1 -12
- package/dist/server/tools/get-churn-metrics.js.map +1 -1
- package/dist/server/tools/get-co-change.d.ts +37 -0
- package/dist/server/tools/get-co-change.d.ts.map +1 -0
- package/dist/server/tools/get-co-change.js +120 -0
- package/dist/server/tools/get-co-change.js.map +1 -0
- package/dist/server/tools/get-context-bundle.d.ts.map +1 -1
- package/dist/server/tools/get-context-bundle.js +56 -3
- package/dist/server/tools/get-context-bundle.js.map +1 -1
- package/dist/server/tools/get-entry-points.d.ts +1 -1
- package/dist/server/tools/get-symbol-risk.d.ts +25 -0
- package/dist/server/tools/get-symbol-risk.d.ts.map +1 -0
- package/dist/server/tools/get-symbol-risk.js +60 -0
- package/dist/server/tools/get-symbol-risk.js.map +1 -0
- package/dist/server/tools/get-symbol-source.d.ts +2 -0
- package/dist/server/tools/get-symbol-source.d.ts.map +1 -1
- package/dist/server/tools/get-symbol-source.js +18 -1
- package/dist/server/tools/get-symbol-source.js.map +1 -1
- package/dist/server/tools/prepare-change.d.ts +61 -0
- package/dist/server/tools/prepare-change.d.ts.map +1 -0
- package/dist/server/tools/prepare-change.js +262 -0
- package/dist/server/tools/prepare-change.js.map +1 -0
- package/dist/server/tools/search-symbols.d.ts +2 -0
- package/dist/server/tools/search-symbols.d.ts.map +1 -1
- package/dist/server/tools/search-symbols.js +33 -0
- package/dist/server/tools/search-symbols.js.map +1 -1
- package/dist/server/tools/symbol-lines.d.ts +25 -0
- package/dist/server/tools/symbol-lines.d.ts.map +1 -0
- package/dist/server/tools/symbol-lines.js +40 -0
- package/dist/server/tools/symbol-lines.js.map +1 -0
- package/dist/server/tools/symbol-risk.d.ts +109 -0
- package/dist/server/tools/symbol-risk.d.ts.map +1 -0
- package/dist/server/tools/symbol-risk.js +251 -0
- package/dist/server/tools/symbol-risk.js.map +1 -0
- package/dist/server/tools/verify-change.d.ts +40 -0
- package/dist/server/tools/verify-change.d.ts.map +1 -0
- package/dist/server/tools/verify-change.js +149 -0
- package/dist/server/tools/verify-change.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/01-introduction.md +2 -2
- package/docs/02-installation.md +97 -89
- package/docs/03-quick-start.md +138 -135
- package/docs/04-configuration.md +247 -214
- package/docs/05-cli-reference.md +236 -219
- package/docs/06-tools-reference.md +902 -499
- package/docs/14-transport-modes.md +170 -167
- package/docs/18-git-history.md +43 -0
- package/docs/23-performance.md +123 -121
- package/docs/26-troubleshooting.md +249 -234
- package/grammars/README.md +88 -0
- package/package.json +7 -25
- package/AGENT_INSTRUCTIONS_SHORT.md +0 -150
package/WORKFLOW-PR-REVIEW.md
CHANGED
|
@@ -1,199 +1,245 @@
|
|
|
1
|
-
# Workflow: Reviewing a Pull Request
|
|
2
|
-
|
|
3
|
-
**Scenario:** It's Tuesday morning. You open GitHub and find a pull request sitting in your queue: "Migrate user authentication to OAuth2 — 40 files changed." You're the designated reviewer. The author is a colleague you trust, but this is a large, cross-cutting change affecting the core authentication flow. You have about 45 minutes before your next meeting.
|
|
4
|
-
|
|
5
|
-
Without context, you'd start reading diffs top to bottom, hoping to understand the intent as you go. With PureContext, you start by understanding the impact, then read strategically.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Step 1: Understand the scope before reading anything
|
|
10
|
-
|
|
11
|
-
**You:** "I have a PR to review — 40 files changed in the authentication migration. Before I read any diffs, give me a symbol-level analysis of what actually changed."
|
|
12
|
-
|
|
13
|
-
**Claude** calls `analyze_diff(base: "main", head: "feature/oauth2-migration")`:
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
Symbols modified (signature changed — potential breaking changes):
|
|
17
|
-
authenticateUser() apps/auth/validator.py signature changed
|
|
18
|
-
require_auth apps/auth/decorators.py implementation changed
|
|
19
|
-
UserPermissions apps/core/types.py fields added
|
|
20
|
-
|
|
21
|
-
Symbols modified (implementation only — no signature change):
|
|
22
|
-
JWTMiddleware apps/auth/middleware.py replaced internally
|
|
23
|
-
createSession() apps/auth/session.py updated token format
|
|
24
|
-
... (12 more)
|
|
25
|
-
|
|
26
|
-
Symbols added (new in this PR):
|
|
27
|
-
OAuthClient apps/auth/oauth2.py new class
|
|
28
|
-
exchangeOAuthCode() apps/auth/oauth2.py new
|
|
29
|
-
refreshOAuthToken() apps/auth/oauth2.py new
|
|
30
|
-
OAuthCallbackView apps/auth/views.py new
|
|
31
|
-
... (8 more)
|
|
32
|
-
|
|
33
|
-
Symbols deleted:
|
|
34
|
-
generateLegacyToken() apps/auth/legacy.py removed
|
|
35
|
-
OldSessionManager apps/auth/sessions.py removed
|
|
36
|
-
validateBasicAuth() apps/auth/basic.py removed
|
|
37
|
-
|
|
38
|
-
Blast radius of modified symbols: 47 files
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
**
|
|
198
|
-
|
|
199
|
-
|
|
1
|
+
# Workflow: Reviewing a Pull Request
|
|
2
|
+
|
|
3
|
+
**Scenario:** It's Tuesday morning. You open GitHub and find a pull request sitting in your queue: "Migrate user authentication to OAuth2 — 40 files changed." You're the designated reviewer. The author is a colleague you trust, but this is a large, cross-cutting change affecting the core authentication flow. You have about 45 minutes before your next meeting.
|
|
4
|
+
|
|
5
|
+
Without context, you'd start reading diffs top to bottom, hoping to understand the intent as you go. With PureContext, you start by understanding the impact, then read strategically.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Step 1: Understand the scope before reading anything
|
|
10
|
+
|
|
11
|
+
**You:** "I have a PR to review — 40 files changed in the authentication migration. Before I read any diffs, give me a symbol-level analysis of what actually changed."
|
|
12
|
+
|
|
13
|
+
**Claude** calls `analyze_diff(base: "main", head: "feature/oauth2-migration")`:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Symbols modified (signature changed — potential breaking changes):
|
|
17
|
+
authenticateUser() apps/auth/validator.py signature changed
|
|
18
|
+
require_auth apps/auth/decorators.py implementation changed
|
|
19
|
+
UserPermissions apps/core/types.py fields added
|
|
20
|
+
|
|
21
|
+
Symbols modified (implementation only — no signature change):
|
|
22
|
+
JWTMiddleware apps/auth/middleware.py replaced internally
|
|
23
|
+
createSession() apps/auth/session.py updated token format
|
|
24
|
+
... (12 more)
|
|
25
|
+
|
|
26
|
+
Symbols added (new in this PR):
|
|
27
|
+
OAuthClient apps/auth/oauth2.py new class
|
|
28
|
+
exchangeOAuthCode() apps/auth/oauth2.py new
|
|
29
|
+
refreshOAuthToken() apps/auth/oauth2.py new
|
|
30
|
+
OAuthCallbackView apps/auth/views.py new
|
|
31
|
+
... (8 more)
|
|
32
|
+
|
|
33
|
+
Symbols deleted:
|
|
34
|
+
generateLegacyToken() apps/auth/legacy.py removed
|
|
35
|
+
OldSessionManager apps/auth/sessions.py removed
|
|
36
|
+
validateBasicAuth() apps/auth/basic.py removed
|
|
37
|
+
|
|
38
|
+
Blast radius of modified symbols: 47 files
|
|
39
|
+
|
|
40
|
+
Aggregate risk: HIGH
|
|
41
|
+
authenticate_user high (78/100) — 6 dependents · churn 9/90d · signature changed
|
|
42
|
+
require_auth high (71/100) — wraps 134 views · co-changes with middleware tests
|
|
43
|
+
|
|
44
|
+
missingCoChange (historically move with the edited files, but NOT in this PR):
|
|
45
|
+
apps/auth/sso_config.py (confidence 0.62) ← SSO config usually changes with the auth flow
|
|
46
|
+
tests/auth/test_sso.py (confidence 0.55)
|
|
47
|
+
|
|
48
|
+
coverageGaps (changed symbols with no detected test):
|
|
49
|
+
OAuthCallbackView apps/auth/views.py
|
|
50
|
+
|
|
51
|
+
recommendedTests:
|
|
52
|
+
tests/auth/test_middleware.py · tests/auth/test_decorators.py
|
|
53
|
+
|
|
54
|
+
architecturalFlags:
|
|
55
|
+
(none — changed files don't currently sit on a cycle or cross a layer boundary)
|
|
56
|
+
|
|
57
|
+
Review priority: CRITICAL (signature breaks + high aggregate risk)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
In 30 seconds you know — without reading a diff:
|
|
61
|
+
- The signature of `authenticateUser` changed and it scores **high** composite risk — everything that calls it must be checked
|
|
62
|
+
- `require_auth` was modified — it wraps 134 view functions, so behavior changes here affect the entire application
|
|
63
|
+
- Three symbols were deleted — are they truly dead, or are there call sites the PR missed?
|
|
64
|
+
- The real blast radius is 47 files, not 40 (some impact is indirect)
|
|
65
|
+
- **`missingCoChange` is the senior-reviewer instinct made explicit:** `sso_config.py` and `test_sso.py` historically move with the auth flow but aren't in this PR — likely an omission to ask the author about
|
|
66
|
+
- `OAuthCallbackView` ships with no test
|
|
67
|
+
|
|
68
|
+
`analyze_diff` returns all of this in one call (the risk / co-change / test / flag sections default on; switch any off for cheap runs). This frames your entire review before you've read a diff.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 2: Verify the deleted symbols were actually unused
|
|
73
|
+
|
|
74
|
+
The deleted symbols are the first concern. Deleting code that's still called causes a runtime error.
|
|
75
|
+
|
|
76
|
+
**You:** "Three symbols were deleted. Verify that they had no importers in main — confirm they were actually dead code before this PR."
|
|
77
|
+
|
|
78
|
+
**Claude** calls `get_blast_radius` on each deleted symbol against the main branch index:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
generateLegacyToken() — 0 importers in main (confirmed dead)
|
|
82
|
+
OldSessionManager — 0 importers in main (confirmed dead)
|
|
83
|
+
validateBasicAuth() — 0 importers in main (confirmed dead)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
All three confirmed dead. The author cleaned up dead code correctly. This concern is closed.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Step 3: Check the signature change for `authenticateUser`
|
|
91
|
+
|
|
92
|
+
A signature change to a function is the highest-risk change in any PR. It means every call site needs to handle the new interface.
|
|
93
|
+
|
|
94
|
+
**You:** "authenticateUser changed its signature. Show me the old signature, the new one, and all the places that call it."
|
|
95
|
+
|
|
96
|
+
**Claude** retrieves the symbol history for `authenticateUser` and the current source:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Old signature (main):
|
|
100
|
+
def authenticate_user(username: str, password: str) -> Optional[User]
|
|
101
|
+
|
|
102
|
+
New signature (this PR):
|
|
103
|
+
def authenticate_user(credentials: OAuthCredentials | PasswordCredentials) -> AuthResult
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The parameter type changed from two positional strings to a union type, and the return type changed from `Optional[User]` to `AuthResult`. This is a significant interface change.
|
|
107
|
+
|
|
108
|
+
**Claude** calls `find_references(symbolId: "authenticateUser")` on the current branch:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
authenticate_user() is called in 6 places:
|
|
112
|
+
|
|
113
|
+
apps/auth/views.py:45 LoginView.post() ← using new signature ✓
|
|
114
|
+
apps/users/tests/test_auth.py:23 test_login() ← updated ✓
|
|
115
|
+
apps/users/tests/test_auth.py:67 test_lockout() ← updated ✓
|
|
116
|
+
apps/api/auth.py:89 api_login() ← using new signature ✓
|
|
117
|
+
apps/admin/views.py:134 admin_login() ← still using old signature ✗
|
|
118
|
+
apps/integrations/sso.py:67 sso_authenticate() ← still using old signature ✗
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Two call sites weren't updated. `admin_login` and `sso_authenticate` are still passing two positional string arguments. This is a bug in the PR — it would pass type checking if the types weren't enforced strictly, but would fail at runtime.
|
|
122
|
+
|
|
123
|
+
You've found the blocking issue in the PR. Two minutes into the review.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Step 4: Verify the critical path
|
|
128
|
+
|
|
129
|
+
`require_auth` is the decorator used by 134 views. It was modified. You need to know the modification is backward compatible.
|
|
130
|
+
|
|
131
|
+
**You:** "require_auth was modified. Show me what changed and verify that all 134 views that use it still work with the new implementation."
|
|
132
|
+
|
|
133
|
+
**Claude** retrieves the new source for `require_auth`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
def require_auth(view_func):
|
|
137
|
+
@wraps(view_func)
|
|
138
|
+
def wrapper(request, *args, **kwargs):
|
|
139
|
+
# NEW: Accept both JWT (legacy) and OAuth2 Bearer tokens
|
|
140
|
+
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
|
141
|
+
if auth_header.startswith('Bearer '):
|
|
142
|
+
user = authenticate_oauth_token(auth_header[7:])
|
|
143
|
+
else:
|
|
144
|
+
user = authenticate_jwt_token(request) # legacy path preserved
|
|
145
|
+
if user is None:
|
|
146
|
+
return HttpResponse(status=401)
|
|
147
|
+
request.user = user
|
|
148
|
+
return view_func(request, *args, **kwargs)
|
|
149
|
+
return wrapper
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The implementation accepts both token formats — OAuth2 Bearer tokens and the legacy JWT. The 134 decorated views don't need to be updated because the decorator handles the transition transparently. This is a good implementation decision, and it's verifiable from the code.
|
|
153
|
+
|
|
154
|
+
**You:** "This looks correct — it's a compatible shim. But check: is there any test that specifically covers the JWT fallback path in the new implementation?"
|
|
155
|
+
|
|
156
|
+
**Claude** calls `search_symbols(query: "authenticate_jwt_token", filePath: "tests/**")`:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
authenticate_jwt_token referenced in tests:
|
|
160
|
+
tests/auth/test_middleware.py test_jwt_fallback() ← exists ✓
|
|
161
|
+
tests/auth/test_decorators.py test_require_auth() ← exists ✓
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The fallback path is tested. The implementation is sound.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Step 5: Check for any missed text-level dependencies
|
|
169
|
+
|
|
170
|
+
Symbol analysis catches structured dependencies. But JWT-specific string patterns — cache keys, token format strings, configuration keys — might be missed.
|
|
171
|
+
|
|
172
|
+
**You:** "Search for any remaining references to the old JWT configuration key or the old session format that might have been missed."
|
|
173
|
+
|
|
174
|
+
**Claude** calls `search_text(query: "JWT_SECRET", filePath: "**/*.py")`:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Remaining JWT_SECRET references:
|
|
178
|
+
apps/auth/oauth2.py:156 settings.JWT_SECRET ← why does the new OAuth module reference this?
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Unexpected. The new OAuth2 module references `JWT_SECRET`. You open the source:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# Temporary: use JWT_SECRET as the OAuth state parameter signing key
|
|
185
|
+
# TODO: Replace with dedicated OAUTH_STATE_SECRET before merge
|
|
186
|
+
state_sig = hmac.new(settings.JWT_SECRET.encode(), ...)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The author left a TODO comment but didn't create a separate configuration key. This is a code smell — the OAuth implementation is reusing the JWT secret for a different purpose, which violates key isolation. Flag it in the review.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Step 6: Did the migration make the architecture worse?
|
|
194
|
+
|
|
195
|
+
A 40-file migration is exactly the kind of change that quietly introduces a new import cycle or layer violation. If a baseline architecture snapshot was taken on `main` (e.g. in CI before the branch diverged), `compare_change_impact` reports only what *this* change introduced.
|
|
196
|
+
|
|
197
|
+
**You:** "Did this PR introduce any new import cycles or layer violations versus main?"
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
compare_change_impact(repoId, baselineSnapshotId: "main-baseline") →
|
|
201
|
+
|
|
202
|
+
verdict: "regressed"
|
|
203
|
+
newCycles: [ [ "apps/auth/oauth2.py", "apps/auth/views.py", "apps/auth/oauth2.py" ] ]
|
|
204
|
+
newLayerViolations: [ ]
|
|
205
|
+
resolvedCycles: [ ]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A new cycle between `oauth2.py` and `views.py` that did not exist on `main` — introduced by this PR. Unlike `analyze_diff`'s `architecturalFlags` (which flag *pre-existing* structure), this is a genuine regression to raise. (The PR author can run the same check locally before pushing — and `verify_change` to confirm they covered the `missingCoChange` files surfaced in Step 1.)
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## The review summary
|
|
213
|
+
|
|
214
|
+
45 minutes, structured review. Findings:
|
|
215
|
+
|
|
216
|
+
| Finding | Severity | Location |
|
|
217
|
+
|---------|----------|----------|
|
|
218
|
+
| `authenticate_user` called with old signature | **Blocking** | `admin/views.py:134`, `integrations/sso.py:67` |
|
|
219
|
+
| New import cycle introduced (oauth2 ⇄ views) | **Blocking** | `auth/oauth2.py` ⇄ `auth/views.py` |
|
|
220
|
+
| JWT secret reused as OAuth state signing key | **Major** — flag before merge | `auth/oauth2.py:156` |
|
|
221
|
+
| SSO config historically co-changes but absent from PR | **Question for author** | `auth/sso_config.py` |
|
|
222
|
+
| `OAuthCallbackView` ships with no test | **Minor** | `auth/views.py` |
|
|
223
|
+
| Deleted symbols confirmed dead | **Pass** | legacy.py, sessions.py, basic.py |
|
|
224
|
+
| `require_auth` shim correctly backward compatible | **Pass** | auth/decorators.py |
|
|
225
|
+
| Legacy fallback path covered by tests | **Pass** | tests/auth/ |
|
|
226
|
+
|
|
227
|
+
You found two real issues — one blocking, one major — before reading most of the diff. The blocking issue (two call sites using the wrong signature) would have been a runtime error in production. The major issue (key reuse) is a security design problem.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## What made this review effective
|
|
232
|
+
|
|
233
|
+
**Start with impact, not diffs.** `analyze_diff` gave you the symbol-level map before you read anything. You knew where to focus.
|
|
234
|
+
|
|
235
|
+
**Verify deletions first.** Deleted code that's still called is the most dangerous class of PR error. Checking blast radius for deleted symbols takes 30 seconds and catches the worst case.
|
|
236
|
+
|
|
237
|
+
**Find all call sites for signature changes.** `find_references` across the branch revealed the two missed call sites that static analysis alone might have missed in a dynamically typed codebase.
|
|
238
|
+
|
|
239
|
+
**Use text search for string-level dependencies.** Symbol analysis doesn't catch JWT_SECRET usage in string context. Text search does.
|
|
240
|
+
|
|
241
|
+
**Know when to stop.** With the two blocking issues identified and the critical path verified, you have what you need to write a meaningful review. You don't need to read all 40 diffs to give a quality review — you need to read the right ones.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
→ Reference: [MCP Tools Reference](docs/06-tools-reference.md) — `analyze_diff`, `compare_change_impact`, `verify_change`, `prepare_change`, `find_references`, `get_blast_radius`, `get_symbol_history`, `search_text`
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Executable launcher for purecontext-mcp.
|
|
4
|
+
*
|
|
5
|
+
* This is intentionally tiny. It imports the Node version guard first (a
|
|
6
|
+
* dependency-free check), and only then dynamically imports the real entry
|
|
7
|
+
* point. The dynamic import means the heavy module graph — including the
|
|
8
|
+
* native `better-sqlite3` binding — is not parsed or evaluated until after
|
|
9
|
+
* the guard has confirmed a supported runtime. On an unsupported Node the
|
|
10
|
+
* guard prints a clear message and exits before anything can crash cryptically.
|
|
11
|
+
*
|
|
12
|
+
* `import(...).catch(...)` (rather than top-level await) is used so this file
|
|
13
|
+
* itself parses on older Node runtimes, letting the guard message through.
|
|
14
|
+
*/
|
|
15
|
+
import './node-guard.js';
|
|
16
|
+
//# sourceMappingURL=bin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAEH,OAAO,iBAAiB,CAAC"}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Executable launcher for purecontext-mcp.
|
|
4
|
+
*
|
|
5
|
+
* This is intentionally tiny. It imports the Node version guard first (a
|
|
6
|
+
* dependency-free check), and only then dynamically imports the real entry
|
|
7
|
+
* point. The dynamic import means the heavy module graph — including the
|
|
8
|
+
* native `better-sqlite3` binding — is not parsed or evaluated until after
|
|
9
|
+
* the guard has confirmed a supported runtime. On an unsupported Node the
|
|
10
|
+
* guard prints a clear message and exits before anything can crash cryptically.
|
|
11
|
+
*
|
|
12
|
+
* `import(...).catch(...)` (rather than top-level await) is used so this file
|
|
13
|
+
* itself parses on older Node runtimes, letting the guard message through.
|
|
14
|
+
*/
|
|
15
|
+
import './node-guard.js';
|
|
16
|
+
import('./index.js').catch((err) => {
|
|
17
|
+
const detail = err instanceof Error && err.stack ? err.stack : String(err);
|
|
18
|
+
process.stderr.write(detail + '\n');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAEH,OAAO,iBAAiB,CAAC;AAEzB,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC1C,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/cli/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/cli/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/cli/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAoHH,wBAAgB,eAAe,IAAI,IAAI,CAetC;AAED,wBAAgB,YAAY,IAAI,IAAI,CAenC;AAID,wBAAgB,aAAa,IAAI,IAAI,CAoDpC;AA6BD,wBAAgB,cAAc,IAAI,IAAI,CAuBrC;AAwCD,oDAAoD;AACpD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAoBvD;AAED,iEAAiE;AACjE,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA8BxD;AA+DD,qEAAqE;AACrE,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAgBvD;AAED,uDAAuD;AACvD,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAoB3D;AAED,8EAA8E;AAC9E,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAG3D;AAuED,qFAAqF;AACrF,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAwC1D;AAED,8EAA8E;AAC9E,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CA8C1D;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAUpD"}
|
package/dist/cli/hooks.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* Also injects PureContext agent instructions into ~/.claude/CLAUDE.md.
|
|
8
8
|
*/
|
|
9
9
|
import { spawnSync } from 'child_process';
|
|
10
|
-
import { createRequire } from 'module';
|
|
11
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'fs';
|
|
11
|
+
import { getSqliteFactory } from '../core/db/sqlite-loader.js';
|
|
12
|
+
import { resolveServerLaunch } from './resolve-node.js';
|
|
12
13
|
import { join, dirname, resolve } from 'path';
|
|
13
14
|
import { homedir } from 'os';
|
|
14
15
|
import { fileURLToPath } from 'url';
|
|
@@ -21,10 +22,16 @@ const CLAUDE_MD_PATH = join(CLAUDE_DIR, 'CLAUDE.md');
|
|
|
21
22
|
// ─── Hook command builder ─────────────────────────────────────────────────────
|
|
22
23
|
// Builds a direct `node "<script>" <subcommand>` invocation that bypasses npx
|
|
23
24
|
// and avoids npm registry SSL checks entirely (important on corporate proxies).
|
|
25
|
+
//
|
|
26
|
+
// The Node binary is the user's *global/default* Node (Volta default, else the
|
|
27
|
+
// system Node) — NOT process.execPath, which would pin hooks to whatever Node
|
|
28
|
+
// happened to run `install` (e.g. a project-pinned version under Volta). Hooks
|
|
29
|
+
// are global tools and must be independent of the project they're installed from.
|
|
24
30
|
function makeHookCmd(subcommand) {
|
|
25
31
|
const cliScript = resolve(__dirname, '..', 'index.js');
|
|
32
|
+
const nodeBin = resolveServerLaunch().command;
|
|
26
33
|
const q = (p) => `"${p}"`;
|
|
27
|
-
return `${q(
|
|
34
|
+
return `${q(nodeBin)} ${q(cliScript)} ${subcommand}`;
|
|
28
35
|
}
|
|
29
36
|
// ─── CLAUDE.md block ──────────────────────────────────────────────────────────
|
|
30
37
|
const CLAUDE_MD_BLOCK = `<!-- purecontext-mcp-start -->
|
|
@@ -103,7 +110,7 @@ export function cmdHooksInstall() {
|
|
|
103
110
|
mergeSettings();
|
|
104
111
|
injectClaudeMd();
|
|
105
112
|
console.log('\nHooks installed. Reopen Claude Code to activate them.\n');
|
|
106
|
-
console.log('Hooks registered (via
|
|
113
|
+
console.log('Hooks registered (invoked directly via your global Node, no npx):');
|
|
107
114
|
console.log(' PostToolUse (hook-posttooluse): re-indexes edited files automatically');
|
|
108
115
|
console.log(' PreCompact (hook-precompact): injects repo state before context compaction');
|
|
109
116
|
console.log(' PreToolUse (hook-pretooluse): suggests PureContext read tools before editing');
|
|
@@ -294,16 +301,17 @@ function readIndexedRepos() {
|
|
|
294
301
|
const indexDir = join(base, 'indexes');
|
|
295
302
|
if (!existsSync(indexDir))
|
|
296
303
|
return [];
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
// Hooks run as short-lived processes without bootstrap, so only the sync
|
|
305
|
+
// (native) backend is reachable here. On a WASM-only Node this returns [] —
|
|
306
|
+
// the snapshot hint is advisory; the index DBs remain fully usable via the
|
|
307
|
+
// server, which initialises the WASM backend properly.
|
|
308
|
+
let factory;
|
|
299
309
|
try {
|
|
300
|
-
|
|
310
|
+
factory = getSqliteFactory();
|
|
301
311
|
}
|
|
302
312
|
catch {
|
|
303
313
|
return [];
|
|
304
314
|
}
|
|
305
|
-
if (!Database)
|
|
306
|
-
return [];
|
|
307
315
|
const repos = [];
|
|
308
316
|
let files;
|
|
309
317
|
try {
|
|
@@ -315,7 +323,7 @@ function readIndexedRepos() {
|
|
|
315
323
|
for (const file of files) {
|
|
316
324
|
let db;
|
|
317
325
|
try {
|
|
318
|
-
db =
|
|
326
|
+
db = factory.open(join(indexDir, file), { readonly: true });
|
|
319
327
|
const rows = db.prepare('SELECT id, root_path, file_count, indexed_at FROM repos LIMIT 50').all();
|
|
320
328
|
repos.push(...rows);
|
|
321
329
|
}
|
|
@@ -400,16 +408,13 @@ function readRepoStats() {
|
|
|
400
408
|
const indexDir = join(base, 'indexes');
|
|
401
409
|
if (!existsSync(indexDir))
|
|
402
410
|
return [];
|
|
403
|
-
|
|
404
|
-
let Database = null;
|
|
411
|
+
let factory;
|
|
405
412
|
try {
|
|
406
|
-
|
|
413
|
+
factory = getSqliteFactory();
|
|
407
414
|
}
|
|
408
415
|
catch {
|
|
409
416
|
return [];
|
|
410
417
|
}
|
|
411
|
-
if (!Database)
|
|
412
|
-
return [];
|
|
413
418
|
let files;
|
|
414
419
|
try {
|
|
415
420
|
files = readdirSync(indexDir).filter((f) => f.endsWith('.db'));
|
|
@@ -421,7 +426,7 @@ function readRepoStats() {
|
|
|
421
426
|
for (const file of files) {
|
|
422
427
|
let db;
|
|
423
428
|
try {
|
|
424
|
-
db =
|
|
429
|
+
db = factory.open(join(indexDir, file), { readonly: true });
|
|
425
430
|
const repo = db.prepare('SELECT id, root_path, file_count, indexed_at FROM repos LIMIT 1').get();
|
|
426
431
|
if (!repo)
|
|
427
432
|
continue;
|