Skip to content

Commit e8c126d

Browse files
authored
Merge pull request #9 from supermemoryai/12-17-feat_add_ispartial_detection_filepath_language_to_context
feat: add isPartial detection, filepath/language to context
2 parents 8c606e9 + e4ad3bd commit e8c126d

File tree

3 files changed

+234
-19
lines changed

3 files changed

+234
-19
lines changed

src/context/index.ts

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import type {
55
ByteRange,
66
Chunk,
77
ChunkContext,
8+
ChunkEntityInfo,
89
ChunkOptions,
910
EntityInfo,
1011
ExtractedEntity,
1112
ImportInfo,
13+
Language,
1214
ScopeTree,
1315
} from '../types'
1416
import { getSiblings, type SiblingOptions } from './siblings'
@@ -68,6 +70,26 @@ export const getScopeForRange = (
6870
return scopeChain
6971
}
7072

73+
/**
74+
* Check if an entity is partial (not fully contained) within a byte range
75+
*
76+
* An entity is partial if it overlaps with the range but is not fully contained.
77+
*
78+
* @param entity - The entity to check
79+
* @param byteRange - The chunk's byte range
80+
* @returns Whether the entity is partial
81+
*/
82+
const isEntityPartial = (
83+
entity: ExtractedEntity,
84+
byteRange: ByteRange,
85+
): boolean => {
86+
// Entity is partial if it starts before the range or ends after the range
87+
return (
88+
entity.byteRange.start < byteRange.start ||
89+
entity.byteRange.end > byteRange.end
90+
)
91+
}
92+
7193
/**
7294
* Get entities within a byte range
7395
*
@@ -76,7 +98,7 @@ export const getScopeForRange = (
7698
*
7799
* @param byteRange - The byte range to search
78100
* @param scopeTree - The scope tree
79-
* @returns Entity info array for entities in range
101+
* @returns Entity info array for entities in range with isPartial detection
80102
*/
81103
export const getEntitiesInRange = (
82104
byteRange: ByteRange,
@@ -90,12 +112,17 @@ export const getEntitiesInRange = (
90112
)
91113
})
92114

93-
// Map to EntityInfo
94-
return overlappingEntities.map((entity) => ({
95-
name: entity.name,
96-
type: entity.type,
97-
signature: entity.signature,
98-
}))
115+
// Map to ChunkEntityInfo with additional fields
116+
return overlappingEntities.map(
117+
(entity): ChunkEntityInfo => ({
118+
name: entity.name,
119+
type: entity.type,
120+
signature: entity.signature,
121+
docstring: entity.docstring,
122+
lineRange: entity.lineRange,
123+
isPartial: isEntityPartial(entity, byteRange),
124+
}),
125+
)
99126
}
100127

101128
/**
@@ -169,23 +196,38 @@ export const getRelevantImports = (
169196
return filteredImports.map(mapToImportInfo)
170197
}
171198

199+
/**
200+
* Options for attaching context to a chunk
201+
*/
202+
export interface AttachContextOptions {
203+
/** The rebuilt text info for the chunk */
204+
text: RebuiltText
205+
/** The scope tree for the file */
206+
scopeTree: ScopeTree
207+
/** Chunking options */
208+
options: ChunkOptions
209+
/** The chunk index */
210+
index: number
211+
/** Total number of chunks */
212+
totalChunks: number
213+
/** File path of the source file (optional) */
214+
filepath?: string
215+
/** Programming language of the source (optional) */
216+
language?: Language
217+
}
218+
172219
/**
173220
* Attach context information to a chunk
174221
*
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
222+
* @param opts - Options containing all parameters for context attachment
180223
* @returns Effect yielding the complete chunk with context
181224
*/
182225
export const attachContext = (
183-
text: RebuiltText,
184-
scopeTree: ScopeTree,
185-
options: ChunkOptions,
186-
index: number,
187-
totalChunks: number,
226+
opts: AttachContextOptions,
188227
): Effect.Effect<Chunk, ContextError> => {
228+
const { text, scopeTree, options, index, totalChunks, filepath, language } =
229+
opts
230+
189231
return Effect.try({
190232
try: () => {
191233
// Determine context mode
@@ -194,6 +236,8 @@ export const attachContext = (
194236
// For 'none' mode, return minimal context
195237
if (contextMode === 'none') {
196238
const context: ChunkContext = {
239+
filepath,
240+
language,
197241
scope: [],
198242
entities: [],
199243
siblings: [],
@@ -228,6 +272,8 @@ export const attachContext = (
228272
const imports = getRelevantImports(entities, scopeTree, filterImports)
229273

230274
const context: ChunkContext = {
275+
filepath,
276+
language,
231277
scope,
232278
entities,
233279
siblings,

src/types.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,19 @@ export interface EntityInfo {
158158
signature?: string
159159
}
160160

161+
/**
162+
* Extended entity info for entities within a chunk
163+
* Includes additional context like docstring, line range, and partial status
164+
*/
165+
export interface ChunkEntityInfo extends EntityInfo {
166+
/** Documentation comment if present */
167+
docstring?: string | null
168+
/** Line range in source (0-indexed, inclusive) */
169+
lineRange?: LineRange
170+
/** Whether this entity spans multiple chunks (is partial) */
171+
isPartial?: boolean
172+
}
173+
161174
/**
162175
* Information about a sibling entity
163176
*/
@@ -190,10 +203,14 @@ export interface ImportInfo {
190203
* Context information for a chunk
191204
*/
192205
export interface ChunkContext {
193-
/** Scope information */
206+
/** File path of the source file */
207+
filepath?: string
208+
/** Programming language of the source */
209+
language?: Language
210+
/** Scope information (scope chain from current to root) */
194211
scope: EntityInfo[]
195212
/** Entities within this chunk */
196-
entities: EntityInfo[]
213+
entities: ChunkEntityInfo[]
197214
/** Nearby sibling entities */
198215
siblings: SiblingInfo[]
199216
/** Relevant imports */

test/scope.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,155 @@ func subtract(a, b int) int {
468468
expect(cls).toBeDefined()
469469
})
470470
})
471+
472+
// ============================================================================
473+
// Context Attachment Tests
474+
// ============================================================================
475+
476+
describe('context attachment', () => {
477+
test('getEntitiesInRange returns entities with isPartial flag', async () => {
478+
const code = `function foo() { return 1 }
479+
function bar() { return 2 }
480+
function baz() { return 3 }`
481+
const entities = await getEntities(code, 'typescript')
482+
const tree = buildScopeTreeFromEntities(entities)
483+
484+
// Import the function we need to test
485+
const { getEntitiesInRange } = await import('../src/context/index')
486+
487+
// Get entities for a range that fully contains 'bar' but not 'foo' or 'baz'
488+
const barEntity = entities.find((e) => e.name === 'bar')
489+
if (barEntity) {
490+
const entitiesInRange = getEntitiesInRange(barEntity.byteRange, tree)
491+
492+
// Should find bar
493+
const bar = entitiesInRange.find((e) => e.name === 'bar')
494+
expect(bar).toBeDefined()
495+
// bar should NOT be partial since we're using its exact range
496+
expect(bar?.isPartial).toBe(false)
497+
}
498+
})
499+
500+
test('getEntitiesInRange marks partial entities correctly', async () => {
501+
const code = `class BigClass {
502+
method1() { return 1 }
503+
method2() { return 2 }
504+
method3() { return 3 }
505+
}`
506+
const entities = await getEntities(code, 'typescript')
507+
const tree = buildScopeTreeFromEntities(entities)
508+
509+
const { getEntitiesInRange } = await import('../src/context/index')
510+
511+
// Get just method2's range - this should be inside BigClass
512+
const method2 = entities.find((e) => e.name === 'method2')
513+
if (method2) {
514+
const entitiesInRange = getEntitiesInRange(method2.byteRange, tree)
515+
516+
// method2 should not be partial (its full range is included)
517+
const m2 = entitiesInRange.find((e) => e.name === 'method2')
518+
expect(m2?.isPartial).toBe(false)
519+
520+
// BigClass should be partial (we only have a slice of it)
521+
const cls = entitiesInRange.find((e) => e.name === 'BigClass')
522+
if (cls) {
523+
expect(cls.isPartial).toBe(true)
524+
}
525+
}
526+
})
527+
528+
test('getEntitiesInRange includes docstring and lineRange', async () => {
529+
const code = `/**
530+
* A test function with docs.
531+
*/
532+
function documented() {
533+
return 1
534+
}`
535+
const entities = await getEntities(code, 'typescript')
536+
const tree = buildScopeTreeFromEntities(entities)
537+
538+
const { getEntitiesInRange } = await import('../src/context/index')
539+
540+
const fn = entities.find((e) => e.name === 'documented')
541+
if (fn) {
542+
const entitiesInRange = getEntitiesInRange(fn.byteRange, tree)
543+
const docFn = entitiesInRange.find((e) => e.name === 'documented')
544+
545+
expect(docFn).toBeDefined()
546+
expect(docFn?.lineRange).toBeDefined()
547+
// Docstring should be present if extracted
548+
if (fn.docstring) {
549+
expect(docFn?.docstring).toContain('test function')
550+
}
551+
}
552+
})
553+
554+
test('attachContext includes filepath and language', async () => {
555+
const { Effect } = await import('effect')
556+
const { attachContext } = await import('../src/context/index')
557+
558+
const code = `function test() { return 1 }`
559+
const entities = await getEntities(code, 'typescript')
560+
const tree = buildScopeTreeFromEntities(entities)
561+
562+
const fn = entities[0]
563+
if (fn) {
564+
const mockText = {
565+
text: code,
566+
byteRange: { start: 0, end: code.length },
567+
lineRange: { start: 0, end: 0 },
568+
}
569+
570+
const chunk = await Effect.runPromise(
571+
attachContext({
572+
text: mockText,
573+
scopeTree: tree,
574+
options: {},
575+
index: 0,
576+
totalChunks: 1,
577+
filepath: 'test.ts',
578+
language: 'typescript',
579+
}),
580+
)
581+
582+
expect(chunk.context.filepath).toBe('test.ts')
583+
expect(chunk.context.language).toBe('typescript')
584+
}
585+
})
586+
587+
test('attachContext respects contextMode none', async () => {
588+
const { Effect } = await import('effect')
589+
const { attachContext } = await import('../src/context/index')
590+
591+
const code = `function test() { return 1 }`
592+
const entities = await getEntities(code, 'typescript')
593+
const tree = buildScopeTreeFromEntities(entities)
594+
595+
const mockText = {
596+
text: code,
597+
byteRange: { start: 0, end: code.length },
598+
lineRange: { start: 0, end: 0 },
599+
}
600+
601+
const chunk = await Effect.runPromise(
602+
attachContext({
603+
text: mockText,
604+
scopeTree: tree,
605+
options: { contextMode: 'none' },
606+
index: 0,
607+
totalChunks: 1,
608+
filepath: 'test.ts',
609+
language: 'typescript',
610+
}),
611+
)
612+
613+
// Even in 'none' mode, filepath and language should be present
614+
expect(chunk.context.filepath).toBe('test.ts')
615+
expect(chunk.context.language).toBe('typescript')
616+
// But scope, entities, siblings, imports should be empty
617+
expect(chunk.context.scope).toEqual([])
618+
expect(chunk.context.entities).toEqual([])
619+
expect(chunk.context.siblings).toEqual([])
620+
expect(chunk.context.imports).toEqual([])
621+
})
622+
})

0 commit comments

Comments
 (0)