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.
Files changed (156) hide show
  1. package/AGENT_INSTRUCTIONS.md +18 -10
  2. package/AGENT_REFERENCE.md +684 -561
  3. package/CHANGELOG.md +567 -445
  4. package/CODE-HISTORY.md +29 -1
  5. package/FULL-INSTALLATION-GUIDE.md +351 -341
  6. package/README.md +411 -339
  7. package/REFACTORING-SAFELY.md +338 -279
  8. package/SAFE-CHANGES.md +208 -156
  9. package/USER-GUIDE.md +3 -1
  10. package/WHY-PURECONTEXT.md +103 -73
  11. package/WORKFLOW-PR-REVIEW.md +245 -199
  12. package/dist/bin.d.ts +16 -0
  13. package/dist/bin.d.ts.map +1 -0
  14. package/dist/bin.js +21 -0
  15. package/dist/bin.js.map +1 -0
  16. package/dist/cli/hooks.d.ts.map +1 -1
  17. package/dist/cli/hooks.js +20 -15
  18. package/dist/cli/hooks.js.map +1 -1
  19. package/dist/cli/install-writers.d.ts.map +1 -1
  20. package/dist/cli/install-writers.js +281 -36
  21. package/dist/cli/install-writers.js.map +1 -1
  22. package/dist/cli/resolve-node.d.ts +53 -0
  23. package/dist/cli/resolve-node.d.ts.map +1 -0
  24. package/dist/cli/resolve-node.js +84 -0
  25. package/dist/cli/resolve-node.js.map +1 -0
  26. package/dist/config/config-loader.js +24 -0
  27. package/dist/config/config-loader.js.map +1 -1
  28. package/dist/config/config-schema.d.ts +71 -0
  29. package/dist/config/config-schema.d.ts.map +1 -1
  30. package/dist/config/config-schema.js +102 -0
  31. package/dist/config/config-schema.js.map +1 -1
  32. package/dist/core/db/api-keys.d.ts +1 -1
  33. package/dist/core/db/api-keys.d.ts.map +1 -1
  34. package/dist/core/db/api-keys.js +39 -39
  35. package/dist/core/db/api-keys.js.map +1 -1
  36. package/dist/core/db/co-change-store.d.ts +34 -0
  37. package/dist/core/db/co-change-store.d.ts.map +1 -0
  38. package/dist/core/db/co-change-store.js +78 -0
  39. package/dist/core/db/co-change-store.js.map +1 -0
  40. package/dist/core/db/schema.d.ts +3 -3
  41. package/dist/core/db/schema.d.ts.map +1 -1
  42. package/dist/core/db/schema.js +12 -30
  43. package/dist/core/db/schema.js.map +1 -1
  44. package/dist/core/db/sqlite-loader.d.ts +51 -0
  45. package/dist/core/db/sqlite-loader.d.ts.map +1 -0
  46. package/dist/core/db/sqlite-loader.js +94 -0
  47. package/dist/core/db/sqlite-loader.js.map +1 -0
  48. package/dist/core/db/wasm-sqlite.d.ts +4 -0
  49. package/dist/core/db/wasm-sqlite.d.ts.map +1 -0
  50. package/dist/core/db/wasm-sqlite.js +270 -0
  51. package/dist/core/db/wasm-sqlite.js.map +1 -0
  52. package/dist/core/diff-parser.d.ts.map +1 -1
  53. package/dist/core/diff-parser.js +6 -1
  54. package/dist/core/diff-parser.js.map +1 -1
  55. package/dist/core/git-log-reader.d.ts +28 -0
  56. package/dist/core/git-log-reader.d.ts.map +1 -1
  57. package/dist/core/git-log-reader.js +74 -3
  58. package/dist/core/git-log-reader.js.map +1 -1
  59. package/dist/core/index-manager.d.ts.map +1 -1
  60. package/dist/core/index-manager.js +20 -1
  61. package/dist/core/index-manager.js.map +1 -1
  62. package/dist/graph/path-resolver.js +86 -17
  63. package/dist/graph/path-resolver.js.map +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +9 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/node-guard.d.ts +15 -0
  69. package/dist/node-guard.d.ts.map +1 -0
  70. package/dist/node-guard.js +33 -0
  71. package/dist/node-guard.js.map +1 -0
  72. package/dist/server/admin-api.d.ts +1 -1
  73. package/dist/server/admin-api.d.ts.map +1 -1
  74. package/dist/server/admin-api.js +2 -2
  75. package/dist/server/admin-api.js.map +1 -1
  76. package/dist/server/auth/api-key.d.ts +1 -1
  77. package/dist/server/auth/api-key.d.ts.map +1 -1
  78. package/dist/server/mcp-server.d.ts.map +1 -1
  79. package/dist/server/mcp-server.js +25 -0
  80. package/dist/server/mcp-server.js.map +1 -1
  81. package/dist/server/tools/analyze-diff.d.ts +8 -0
  82. package/dist/server/tools/analyze-diff.d.ts.map +1 -1
  83. package/dist/server/tools/analyze-diff.js +80 -16
  84. package/dist/server/tools/analyze-diff.js.map +1 -1
  85. package/dist/server/tools/change-synthesis.d.ts +90 -0
  86. package/dist/server/tools/change-synthesis.d.ts.map +1 -0
  87. package/dist/server/tools/change-synthesis.js +236 -0
  88. package/dist/server/tools/change-synthesis.js.map +1 -0
  89. package/dist/server/tools/co-change.d.ts +65 -0
  90. package/dist/server/tools/co-change.d.ts.map +1 -0
  91. package/dist/server/tools/co-change.js +146 -0
  92. package/dist/server/tools/co-change.js.map +1 -0
  93. package/dist/server/tools/compare-change-impact.d.ts +58 -0
  94. package/dist/server/tools/compare-change-impact.d.ts.map +1 -0
  95. package/dist/server/tools/compare-change-impact.js +0 -0
  96. package/dist/server/tools/compare-change-impact.js.map +1 -0
  97. package/dist/server/tools/find-refactoring-opportunities.d.ts +1 -1
  98. package/dist/server/tools/get-architecture-snapshot.d.ts.map +1 -1
  99. package/dist/server/tools/get-architecture-snapshot.js +28 -14
  100. package/dist/server/tools/get-architecture-snapshot.js.map +1 -1
  101. package/dist/server/tools/get-churn-metrics.d.ts.map +1 -1
  102. package/dist/server/tools/get-churn-metrics.js +1 -12
  103. package/dist/server/tools/get-churn-metrics.js.map +1 -1
  104. package/dist/server/tools/get-co-change.d.ts +37 -0
  105. package/dist/server/tools/get-co-change.d.ts.map +1 -0
  106. package/dist/server/tools/get-co-change.js +120 -0
  107. package/dist/server/tools/get-co-change.js.map +1 -0
  108. package/dist/server/tools/get-context-bundle.d.ts.map +1 -1
  109. package/dist/server/tools/get-context-bundle.js +56 -3
  110. package/dist/server/tools/get-context-bundle.js.map +1 -1
  111. package/dist/server/tools/get-entry-points.d.ts +1 -1
  112. package/dist/server/tools/get-symbol-risk.d.ts +25 -0
  113. package/dist/server/tools/get-symbol-risk.d.ts.map +1 -0
  114. package/dist/server/tools/get-symbol-risk.js +60 -0
  115. package/dist/server/tools/get-symbol-risk.js.map +1 -0
  116. package/dist/server/tools/get-symbol-source.d.ts +2 -0
  117. package/dist/server/tools/get-symbol-source.d.ts.map +1 -1
  118. package/dist/server/tools/get-symbol-source.js +18 -1
  119. package/dist/server/tools/get-symbol-source.js.map +1 -1
  120. package/dist/server/tools/prepare-change.d.ts +61 -0
  121. package/dist/server/tools/prepare-change.d.ts.map +1 -0
  122. package/dist/server/tools/prepare-change.js +262 -0
  123. package/dist/server/tools/prepare-change.js.map +1 -0
  124. package/dist/server/tools/search-symbols.d.ts +2 -0
  125. package/dist/server/tools/search-symbols.d.ts.map +1 -1
  126. package/dist/server/tools/search-symbols.js +33 -0
  127. package/dist/server/tools/search-symbols.js.map +1 -1
  128. package/dist/server/tools/symbol-lines.d.ts +25 -0
  129. package/dist/server/tools/symbol-lines.d.ts.map +1 -0
  130. package/dist/server/tools/symbol-lines.js +40 -0
  131. package/dist/server/tools/symbol-lines.js.map +1 -0
  132. package/dist/server/tools/symbol-risk.d.ts +109 -0
  133. package/dist/server/tools/symbol-risk.d.ts.map +1 -0
  134. package/dist/server/tools/symbol-risk.js +251 -0
  135. package/dist/server/tools/symbol-risk.js.map +1 -0
  136. package/dist/server/tools/verify-change.d.ts +40 -0
  137. package/dist/server/tools/verify-change.d.ts.map +1 -0
  138. package/dist/server/tools/verify-change.js +149 -0
  139. package/dist/server/tools/verify-change.js.map +1 -0
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.d.ts.map +1 -1
  142. package/dist/version.js +1 -1
  143. package/dist/version.js.map +1 -1
  144. package/docs/01-introduction.md +2 -2
  145. package/docs/02-installation.md +97 -89
  146. package/docs/03-quick-start.md +138 -135
  147. package/docs/04-configuration.md +247 -214
  148. package/docs/05-cli-reference.md +236 -219
  149. package/docs/06-tools-reference.md +902 -499
  150. package/docs/14-transport-modes.md +170 -167
  151. package/docs/18-git-history.md +43 -0
  152. package/docs/23-performance.md +123 -121
  153. package/docs/26-troubleshooting.md +249 -234
  154. package/grammars/README.md +88 -0
  155. package/package.json +7 -25
  156. package/AGENT_INSTRUCTIONS_SHORT.md +0 -150
@@ -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
- Review priority: HIGH (signature changes to authenticateUser and require_auth)
40
- ```
41
-
42
- In 30 seconds you know:
43
- - The signature of `authenticateUser` changed — everything that calls it may need to be checked
44
- - `require_auth` was modified it wraps 134 view functions, so behavior changes here affect the entire application
45
- - Three symbols were deleted are they truly dead, or are there call sites the PR missed?
46
- - The real blast radius is 47 files, not 40 (some impact is indirect)
47
-
48
- This frames your entire review before you've read a diff.
49
-
50
- ---
51
-
52
- ## Step 2: Verify the deleted symbols were actually unused
53
-
54
- The deleted symbols are the first concern. Deleting code that's still called causes a runtime error.
55
-
56
- **You:** "Three symbols were deleted. Verify that they had no importers in main — confirm they were actually dead code before this PR."
57
-
58
- **Claude** calls `get_blast_radius` on each deleted symbol against the main branch index:
59
-
60
- ```
61
- generateLegacyToken() 0 importers in main (confirmed dead)
62
- OldSessionManager 0 importers in main (confirmed dead)
63
- validateBasicAuth() 0 importers in main (confirmed dead)
64
- ```
65
-
66
- All three confirmed dead. The author cleaned up dead code correctly. This concern is closed.
67
-
68
- ---
69
-
70
- ## Step 3: Check the signature change for `authenticateUser`
71
-
72
- 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.
73
-
74
- **You:** "authenticateUser changed its signature. Show me the old signature, the new one, and all the places that call it."
75
-
76
- **Claude** retrieves the symbol history for `authenticateUser` and the current source:
77
-
78
- ```
79
- Old signature (main):
80
- def authenticate_user(username: str, password: str) -> Optional[User]
81
-
82
- New signature (this PR):
83
- def authenticate_user(credentials: OAuthCredentials | PasswordCredentials) -> AuthResult
84
- ```
85
-
86
- 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.
87
-
88
- **Claude** calls `find_references(symbolId: "authenticateUser")` on the current branch:
89
-
90
- ```
91
- authenticate_user() is called in 6 places:
92
-
93
- apps/auth/views.py:45 LoginView.post() ← using new signature ✓
94
- apps/users/tests/test_auth.py:23 test_login() ← updated
95
- apps/users/tests/test_auth.py:67 test_lockout() ← updated ✓
96
- apps/api/auth.py:89 api_login() ← using new signature
97
- apps/admin/views.py:134 admin_login() ← still using old signature ✗
98
- apps/integrations/sso.py:67 sso_authenticate() ← still using old signature ✗
99
- ```
100
-
101
- 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.
102
-
103
- You've found the blocking issue in the PR. Two minutes into the review.
104
-
105
- ---
106
-
107
- ## Step 4: Verify the critical path
108
-
109
- `require_auth` is the decorator used by 134 views. It was modified. You need to know the modification is backward compatible.
110
-
111
- **You:** "require_auth was modified. Show me what changed and verify that all 134 views that use it still work with the new implementation."
112
-
113
- **Claude** retrieves the new source for `require_auth`:
114
-
115
- ```python
116
- def require_auth(view_func):
117
- @wraps(view_func)
118
- def wrapper(request, *args, **kwargs):
119
- # NEW: Accept both JWT (legacy) and OAuth2 Bearer tokens
120
- auth_header = request.META.get('HTTP_AUTHORIZATION', '')
121
- if auth_header.startswith('Bearer '):
122
- user = authenticate_oauth_token(auth_header[7:])
123
- else:
124
- user = authenticate_jwt_token(request) # legacy path preserved
125
- if user is None:
126
- return HttpResponse(status=401)
127
- request.user = user
128
- return view_func(request, *args, **kwargs)
129
- return wrapper
130
- ```
131
-
132
- 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.
133
-
134
- **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?"
135
-
136
- **Claude** calls `search_symbols(query: "authenticate_jwt_token", filePath: "tests/**")`:
137
-
138
- ```
139
- authenticate_jwt_token referenced in tests:
140
- tests/auth/test_middleware.py test_jwt_fallback() ← exists ✓
141
- tests/auth/test_decorators.py test_require_auth() ← exists ✓
142
- ```
143
-
144
- The fallback path is tested. The implementation is sound.
145
-
146
- ---
147
-
148
- ## Step 5: Check for any missed text-level dependencies
149
-
150
- Symbol analysis catches structured dependencies. But JWT-specific string patterns — cache keys, token format strings, configuration keys — might be missed.
151
-
152
- **You:** "Search for any remaining references to the old JWT configuration key or the old session format that might have been missed."
153
-
154
- **Claude** calls `search_text(query: "JWT_SECRET", filePath: "**/*.py")`:
155
-
156
- ```
157
- Remaining JWT_SECRET references:
158
- apps/auth/oauth2.py:156 settings.JWT_SECRET ← why does the new OAuth module reference this?
159
- ```
160
-
161
- Unexpected. The new OAuth2 module references `JWT_SECRET`. You open the source:
162
-
163
- ```python
164
- # Temporary: use JWT_SECRET as the OAuth state parameter signing key
165
- # TODO: Replace with dedicated OAUTH_STATE_SECRET before merge
166
- state_sig = hmac.new(settings.JWT_SECRET.encode(), ...)
167
- ```
168
-
169
- 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.
170
-
171
- ---
172
-
173
- ## The review summary
174
-
175
- 45 minutes, structured review. Findings:
176
-
177
- | Finding | Severity | Location |
178
- |---------|----------|----------|
179
- | `authenticate_user` called with old signature | **Blocking** | `admin/views.py:134`, `integrations/sso.py:67` |
180
- | JWT secret reused as OAuth state signing key | **Major** — flag before merge | `auth/oauth2.py:156` |
181
- | Deleted symbols confirmed dead | **Pass** | legacy.py, sessions.py, basic.py |
182
- | `require_auth` shim correctly backward compatible | **Pass** | auth/decorators.py |
183
- | Legacy fallback path covered by tests | **Pass** | tests/auth/ |
184
-
185
- 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.
186
-
187
- ---
188
-
189
- ## What made this review effective
190
-
191
- **Start with impact, not diffs.** `analyze_diff` gave you the symbol-level map before you read anything. You knew where to focus.
192
-
193
- **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.
194
-
195
- **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.
196
-
197
- **Use text search for string-level dependencies.** Symbol analysis doesn't catch JWT_SECRET usage in string context. Text search does.
198
-
199
- **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.
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
@@ -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"}
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/cli/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA+GH,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;AA6DD,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;AAyED,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"}
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(process.execPath)} ${q(cliScript)} ${subcommand}`;
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 npx purecontext-mcp hook-*):');
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
- const _require = createRequire(import.meta.url);
298
- let Database = null;
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
- Database = _require('better-sqlite3');
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 = new Database(join(indexDir, file), { readonly: true });
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
- const _require = createRequire(import.meta.url);
404
- let Database = null;
411
+ let factory;
405
412
  try {
406
- Database = _require('better-sqlite3');
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 = new Database(join(indexDir, file), { readonly: true });
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;