Skip to content

Commit 8c606e9

Browse files
authored
Merge pull request #7 from supermemoryai/12-17-feat_implement_scope_tree_construction_system
feat: implement scope tree construction system
2 parents 418c02d + 62a6ff9 commit 8c606e9

File tree

10 files changed

+1323
-136
lines changed

10 files changed

+1323
-136
lines changed

src/context/index.ts

Lines changed: 189 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { Effect } from 'effect'
22
import type { RebuiltText } from '../chunking/rebuild'
3+
import { findScopeAtOffset, getAncestorChain } from '../scope/tree'
34
import type {
45
ByteRange,
56
Chunk,
67
ChunkContext,
78
ChunkOptions,
9+
EntityInfo,
10+
ExtractedEntity,
11+
ImportInfo,
812
ScopeTree,
913
} from '../types'
14+
import { getSiblings, type SiblingOptions } from './siblings'
1015

1116
/**
1217
* Error when attaching context fails
@@ -19,88 +24,90 @@ export class ContextError {
1924
) {}
2025
}
2126

22-
/**
23-
* Attach context information to a chunk
24-
*
25-
* @param text - The rebuilt text info for the chunk
26-
* @param scopeTree - The scope tree for the file
27-
* @param options - Chunking options
28-
* @param index - The chunk index
29-
* @param totalChunks - Total number of chunks
30-
* @returns Effect yielding the complete chunk with context
31-
*
32-
* TODO: Implement context attachment
33-
*/
34-
export const attachContext = (
35-
text: RebuiltText,
36-
scopeTree: ScopeTree,
37-
options: ChunkOptions,
38-
index: number,
39-
totalChunks: number,
40-
): Effect.Effect<Chunk, ContextError> => {
41-
// TODO: Implement context attachment
42-
// 1. Find scope for this chunk's byte range
43-
// 2. Get entities within the chunk
44-
// 3. Get siblings based on options
45-
// 4. Get relevant imports
46-
const context: ChunkContext = {
47-
scope: [],
48-
entities: [],
49-
siblings: [],
50-
imports: [],
51-
}
52-
53-
void scopeTree
54-
void options
55-
56-
return Effect.succeed({
57-
text: text.text,
58-
byteRange: text.byteRange,
59-
lineRange: text.lineRange,
60-
context,
61-
index,
62-
totalChunks,
63-
})
64-
}
65-
6627
/**
6728
* Get scope information for a byte range
6829
*
30+
* Finds the scope containing this range and builds an array of EntityInfo
31+
* from the scope and its ancestors.
32+
*
6933
* @param byteRange - The byte range to get scope for
7034
* @param scopeTree - The scope tree
71-
* @returns Scope entity info array
72-
*
73-
* TODO: Implement scope lookup
35+
* @returns Scope entity info array representing the scope chain
7436
*/
7537
export const getScopeForRange = (
7638
byteRange: ByteRange,
7739
scopeTree: ScopeTree,
7840
): ChunkContext['scope'] => {
79-
// TODO: Implement scope lookup
80-
// Find containing scopes and return as EntityInfo[]
81-
void byteRange
82-
void scopeTree
83-
return []
41+
// Find the scope at the start of the range
42+
const scope = findScopeAtOffset(scopeTree, byteRange.start)
43+
44+
if (!scope) {
45+
return []
46+
}
47+
48+
// Build scope chain: current scope + ancestors
49+
const scopeChain: EntityInfo[] = []
50+
51+
// Add current scope
52+
scopeChain.push({
53+
name: scope.entity.name,
54+
type: scope.entity.type,
55+
signature: scope.entity.signature,
56+
})
57+
58+
// Add ancestors (from immediate parent to root)
59+
const ancestors = getAncestorChain(scope)
60+
for (const ancestor of ancestors) {
61+
scopeChain.push({
62+
name: ancestor.entity.name,
63+
type: ancestor.entity.type,
64+
signature: ancestor.entity.signature,
65+
})
66+
}
67+
68+
return scopeChain
8469
}
8570

8671
/**
8772
* Get entities within a byte range
8873
*
74+
* Finds entities whose byte ranges overlap with the given range.
75+
* Overlap condition: entity.start < range.end && entity.end > range.start
76+
*
8977
* @param byteRange - The byte range to search
9078
* @param scopeTree - The scope tree
9179
* @returns Entity info array for entities in range
92-
*
93-
* TODO: Implement entity lookup
9480
*/
9581
export const getEntitiesInRange = (
9682
byteRange: ByteRange,
9783
scopeTree: ScopeTree,
9884
): ChunkContext['entities'] => {
99-
// TODO: Implement entity lookup
100-
// Find entities whose ranges overlap with byteRange
101-
void byteRange
102-
void scopeTree
103-
return []
85+
const overlappingEntities = scopeTree.allEntities.filter((entity) => {
86+
// Overlap check: entity.start < range.end && entity.end > range.start
87+
return (
88+
entity.byteRange.start < byteRange.end &&
89+
entity.byteRange.end > byteRange.start
90+
)
91+
})
92+
93+
// Map to EntityInfo
94+
return overlappingEntities.map((entity) => ({
95+
name: entity.name,
96+
type: entity.type,
97+
signature: entity.signature,
98+
}))
99+
}
100+
101+
/**
102+
* Get import source from an import entity
103+
*
104+
* Uses the pre-extracted source from AST parsing (works for all languages).
105+
*
106+
* @param entity - The import entity
107+
* @returns The import source or empty string if not found
108+
*/
109+
const getImportSource = (entity: ExtractedEntity): string => {
110+
return entity.source ?? ''
104111
}
105112

106113
/**
@@ -110,18 +117,133 @@ export const getEntitiesInRange = (
110117
* @param scopeTree - The scope tree
111118
* @param filterImports - Whether to filter to only used imports
112119
* @returns Import info array
113-
*
114-
* TODO: Implement import filtering
115120
*/
116121
export const getRelevantImports = (
117122
entities: ChunkContext['entities'],
118123
scopeTree: ScopeTree,
119124
filterImports: boolean,
120125
): ChunkContext['imports'] => {
121-
// TODO: Implement import filtering
122-
// If filterImports, only include imports used by chunk entities
123-
void entities
124-
void scopeTree
125-
void filterImports
126-
return []
126+
const imports = scopeTree.imports
127+
128+
if (imports.length === 0) {
129+
return []
130+
}
131+
132+
// Map import entity to ImportInfo
133+
const mapToImportInfo = (entity: ExtractedEntity): ImportInfo => ({
134+
name: entity.name,
135+
source: getImportSource(entity),
136+
})
137+
138+
// If not filtering, return all imports
139+
if (!filterImports) {
140+
return imports.map(mapToImportInfo)
141+
}
142+
143+
// Filter to only imports that are used by entities in the chunk
144+
// Build a set of names that appear in entity signatures and names
145+
const usedNames = new Set<string>()
146+
for (const entity of entities) {
147+
// Add the entity name
148+
usedNames.add(entity.name)
149+
150+
// Extract identifiers from signature if available
151+
if (entity.signature) {
152+
// Match word characters that could be identifiers
153+
const identifiers = entity.signature.match(
154+
/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g,
155+
)
156+
if (identifiers) {
157+
for (const id of identifiers) {
158+
usedNames.add(id)
159+
}
160+
}
161+
}
162+
}
163+
164+
// Filter imports to those whose names appear in the chunk
165+
const filteredImports = imports.filter((importEntity) => {
166+
return usedNames.has(importEntity.name)
167+
})
168+
169+
return filteredImports.map(mapToImportInfo)
170+
}
171+
172+
/**
173+
* Attach context information to a chunk
174+
*
175+
* @param text - The rebuilt text info for the chunk
176+
* @param scopeTree - The scope tree for the file
177+
* @param options - Chunking options
178+
* @param index - The chunk index
179+
* @param totalChunks - Total number of chunks
180+
* @returns Effect yielding the complete chunk with context
181+
*/
182+
export const attachContext = (
183+
text: RebuiltText,
184+
scopeTree: ScopeTree,
185+
options: ChunkOptions,
186+
index: number,
187+
totalChunks: number,
188+
): Effect.Effect<Chunk, ContextError> => {
189+
return Effect.try({
190+
try: () => {
191+
// Determine context mode
192+
const contextMode = options.contextMode ?? 'full'
193+
194+
// For 'none' mode, return minimal context
195+
if (contextMode === 'none') {
196+
const context: ChunkContext = {
197+
scope: [],
198+
entities: [],
199+
siblings: [],
200+
imports: [],
201+
}
202+
return {
203+
text: text.text,
204+
byteRange: text.byteRange,
205+
lineRange: text.lineRange,
206+
context,
207+
index,
208+
totalChunks,
209+
}
210+
}
211+
212+
// Get scope for this chunk's byte range
213+
const scope = getScopeForRange(text.byteRange, scopeTree)
214+
215+
// Get entities within the chunk
216+
const entities = getEntitiesInRange(text.byteRange, scopeTree)
217+
218+
// Get siblings based on options
219+
const siblingDetail = options.siblingDetail ?? 'signatures'
220+
const siblingOptions: SiblingOptions = {
221+
detail: siblingDetail,
222+
maxSiblings: contextMode === 'minimal' ? 2 : undefined,
223+
}
224+
const siblings = getSiblings(text.byteRange, scopeTree, siblingOptions)
225+
226+
// Get relevant imports
227+
const filterImports = options.filterImports ?? false
228+
const imports = getRelevantImports(entities, scopeTree, filterImports)
229+
230+
const context: ChunkContext = {
231+
scope,
232+
entities,
233+
siblings,
234+
imports,
235+
}
236+
237+
return {
238+
text: text.text,
239+
byteRange: text.byteRange,
240+
lineRange: text.lineRange,
241+
context,
242+
index,
243+
totalChunks,
244+
}
245+
},
246+
catch: (error: unknown) =>
247+
new ContextError('Failed to attach context', error),
248+
})
127249
}

0 commit comments

Comments
 (0)