-
-
Notifications
You must be signed in to change notification settings - Fork 747
Expand file tree
/
Copy pathhook-runtime.js
More file actions
186 lines (162 loc) · 5.48 KB
/
hook-runtime.js
File metadata and controls
186 lines (162 loc) · 5.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
'use strict';
const path = require('path');
const { RegistryProvider } = require('./providers/registry-provider');
/** Cached provider instance (survives across hook invocations in same process). */
let _provider = null;
let _providerRoot = null;
/**
* Get or create a RegistryProvider singleton.
* Resets if projectRoot changes between calls.
* @param {string} projectRoot - Project root directory
* @returns {RegistryProvider}
*/
function getProvider(projectRoot) {
if (!_provider || _providerRoot !== projectRoot) {
_provider = new RegistryProvider({ projectRoot });
_providerRoot = projectRoot;
}
return _provider;
}
/**
* Resolve code intelligence context for a file being written/edited.
*
* Queries RegistryProvider for:
* - Entity definition (path, layer, purpose, type)
* - References (files that use this entity)
* - Dependencies (entities this file depends on)
*
* @param {string} filePath - Absolute or relative path to the target file
* @param {string} cwd - Project root / working directory
* @returns {{ entity: Object|null, references: Array|null, dependencies: Object|null }|null}
*/
async function resolveCodeIntel(filePath, cwd) {
if (!filePath || !cwd) return null;
try {
const provider = getProvider(cwd);
if (!provider.isAvailable()) return null;
// Normalize to relative path (registry uses relative paths)
let relativePath = filePath;
if (path.isAbsolute(filePath)) {
relativePath = path.relative(cwd, filePath).replace(/\\/g, '/');
} else {
relativePath = filePath.replace(/\\/g, '/');
}
// Run all three queries in parallel
const [definition, references, dependencies] = await Promise.all([
provider.findDefinition(relativePath),
provider.findReferences(relativePath),
provider.analyzeDependencies(relativePath),
]);
// Treat empty dependency graph as no data
const hasUsefulDeps = dependencies && dependencies.nodes && dependencies.nodes.length > 0;
// If nothing found at all, try searching by the file basename
if (!definition && !references && !hasUsefulDeps) {
const basename = path.basename(relativePath, path.extname(relativePath));
const fallbackDef = await provider.findDefinition(basename);
if (!fallbackDef) return null;
const [fallbackRefs, fallbackDeps] = await Promise.all([
provider.findReferences(basename),
provider.analyzeDependencies(basename),
]);
return {
entity: fallbackDef,
references: fallbackRefs,
dependencies: fallbackDeps,
};
}
return {
entity: definition,
references,
dependencies,
};
} catch (_err) {
// Guard against provider exceptions to avoid unhandled rejections in hook runtime
return null;
}
}
/**
* Format code intelligence data as XML for injection into Claude context.
*
* @param {Object|null} intel - Result from resolveCodeIntel()
* @param {string} filePath - Target file path (for display)
* @returns {string|null} XML string or null if no data
*/
function formatAsXml(intel, filePath) {
if (!intel) return null;
const { entity, references, dependencies } = intel;
// At least one piece of data must exist
if (!entity && !references && !dependencies) return null;
const lines = ['<code-intel-context>'];
lines.push(` <target-file>${escapeXml(filePath)}</target-file>`);
// Entity definition
if (entity) {
lines.push(' <existing-entity>');
if (entity.file) lines.push(` <path>${escapeXml(entity.file)}</path>`);
if (entity.context) lines.push(` <purpose>${escapeXml(entity.context)}</purpose>`);
lines.push(' </existing-entity>');
}
// References
if (references && references.length > 0) {
// Deduplicate by file path
const uniqueRefs = [];
const seen = new Set();
for (const ref of references) {
if (ref.file && !seen.has(ref.file)) {
seen.add(ref.file);
uniqueRefs.push(ref);
}
}
lines.push(` <referenced-by count="${uniqueRefs.length}">`);
for (const ref of uniqueRefs.slice(0, 15)) {
const ctx = ref.context ? ` context="${escapeXml(ref.context)}"` : '';
lines.push(` <ref file="${escapeXml(ref.file)}"${ctx} />`);
}
if (uniqueRefs.length > 15) {
lines.push(` <!-- ...and ${uniqueRefs.length - 15} more -->`);
}
lines.push(' </referenced-by>');
}
// Dependencies
if (dependencies && dependencies.nodes && dependencies.nodes.length > 1) {
// First node is the target itself, rest are dependencies
const depNodes = dependencies.nodes.slice(1);
lines.push(` <dependencies count="${depNodes.length}">`);
for (const dep of depNodes.slice(0, 10)) {
const layer = dep.layer ? ` layer="${dep.layer}"` : '';
lines.push(` <dep name="${escapeXml(dep.name)}"${layer} />`);
}
if (depNodes.length > 10) {
lines.push(` <!-- ...and ${depNodes.length - 10} more -->`);
}
lines.push(' </dependencies>');
}
lines.push('</code-intel-context>');
return lines.join('\n');
}
/**
* Escape special XML characters.
* @param {string} str
* @returns {string}
*/
function escapeXml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
* Reset cached provider (for testing).
*/
function _resetForTesting() {
_provider = null;
_providerRoot = null;
}
module.exports = {
resolveCodeIntel,
formatAsXml,
escapeXml,
getProvider,
_resetForTesting,
};