@goshenkata/dryscan-core 1.0.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.
@@ -0,0 +1,104 @@
1
+ import { DuplicateGroup } from "../types";
2
+
3
+ /**
4
+ * In-memory cache for duplicate comparison scores.
5
+ * Stores a global map of comparison keys and a per-file index for fast invalidation.
6
+ */
7
+ export class DuplicationCache {
8
+ private static instance: DuplicationCache | null = null;
9
+
10
+ private readonly comparisons = new Map<string, number>();
11
+ private readonly fileIndex = new Map<string, Set<string>>();
12
+ private initialized = false;
13
+
14
+ static getInstance(): DuplicationCache {
15
+ if (!DuplicationCache.instance) {
16
+ DuplicationCache.instance = new DuplicationCache();
17
+ }
18
+ return DuplicationCache.instance;
19
+ }
20
+
21
+ /**
22
+ * Updates the cache with fresh duplicate groups. Not awaited by callers to avoid blocking.
23
+ */
24
+ async update(groups: DuplicateGroup[]): Promise<void> {
25
+ if (!groups) return;
26
+
27
+ for (const group of groups) {
28
+ const key = this.makeKey(group.left.id, group.right.id);
29
+ this.comparisons.set(key, group.similarity);
30
+ this.addKeyForFile(group.left.filePath, key);
31
+ this.addKeyForFile(group.right.filePath, key);
32
+ }
33
+
34
+ this.initialized = this.initialized || groups.length > 0;
35
+ }
36
+
37
+ /**
38
+ * Retrieves a cached similarity if present and valid for both file paths.
39
+ * Returns null when the cache has not been initialized or when the pair is missing.
40
+ */
41
+ get(leftId: string, rightId: string, leftFilePath: string, rightFilePath: string): number | null {
42
+ if (!this.initialized) return null;
43
+
44
+ const key = this.makeKey(leftId, rightId);
45
+ if (!this.fileHasKey(leftFilePath, key) || !this.fileHasKey(rightFilePath, key)) {
46
+ return null;
47
+ }
48
+
49
+ const value = this.comparisons.get(key);
50
+ return typeof value === "number" ? value : null;
51
+ }
52
+
53
+ /**
54
+ * Invalidates all cached comparisons involving the provided file paths.
55
+ */
56
+ async invalidate(paths: string[]): Promise<void> {
57
+ if (!this.initialized || !paths || paths.length === 0) return;
58
+
59
+ const unique = new Set(paths);
60
+ for (const filePath of unique) {
61
+ const keys = this.fileIndex.get(filePath);
62
+ if (!keys) continue;
63
+
64
+ for (const key of keys) {
65
+ this.comparisons.delete(key);
66
+ for (const [otherPath, otherKeys] of this.fileIndex.entries()) {
67
+ if (otherKeys.delete(key) && otherKeys.size === 0) {
68
+ this.fileIndex.delete(otherPath);
69
+ }
70
+ }
71
+ }
72
+
73
+ this.fileIndex.delete(filePath);
74
+ }
75
+
76
+ if (this.comparisons.size === 0) {
77
+ this.initialized = false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Clears all cached data. Intended for test setup.
83
+ */
84
+ clear(): void {
85
+ this.comparisons.clear();
86
+ this.fileIndex.clear();
87
+ this.initialized = false;
88
+ }
89
+
90
+ private addKeyForFile(filePath: string, key: string): void {
91
+ const current = this.fileIndex.get(filePath) ?? new Set<string>();
92
+ current.add(key);
93
+ this.fileIndex.set(filePath, current);
94
+ }
95
+
96
+ private fileHasKey(filePath: string, key: string): boolean {
97
+ const keys = this.fileIndex.get(filePath);
98
+ return keys ? keys.has(key) : false;
99
+ }
100
+
101
+ private makeKey(leftId: string, rightId: string): string {
102
+ return [leftId, rightId].sort().join("::");
103
+ }
104
+ }
@@ -0,0 +1,58 @@
1
+ import debug from "debug";
2
+ import { OllamaEmbeddings } from "@langchain/ollama";
3
+ import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
4
+ import { TaskType } from "@google/generative-ai";
5
+ import { IndexUnit } from "../types";
6
+ import { configStore } from "../config/configStore";
7
+
8
+ const log = debug("DryScan:EmbeddingService");
9
+
10
+ export class EmbeddingService {
11
+ constructor(private readonly repoPath: string) { }
12
+
13
+ async addEmbedding(fn: IndexUnit): Promise<IndexUnit> {
14
+ const config = await configStore.get(this.repoPath);
15
+ const maxContext = config?.contextLength ?? 2048;
16
+ if (fn.code.length > maxContext) {
17
+ log(
18
+ "Skipping embedding for %s (code length %d exceeds context %d)",
19
+ fn.id,
20
+ fn.code.length,
21
+ maxContext
22
+ );
23
+ return { ...fn, embedding: null };
24
+ }
25
+
26
+ const model = config.embeddingModel ?? undefined
27
+ const source = config.embeddingSource
28
+ if (!source) {
29
+ const message = `Embedding source is not configured for repository at ${this.repoPath}`;
30
+ log(message);
31
+ throw new Error(message);
32
+ }
33
+
34
+ const embeddings = this.buildProvider(source, model);
35
+ const embedding = await embeddings.embedQuery(fn.code);
36
+ return { ...fn, embedding };
37
+ }
38
+
39
+ private buildProvider(source: string, model: string) {
40
+ if (source === "google") {
41
+ return new GoogleGenerativeAIEmbeddings({
42
+ model: model ?? "gemini-embedding-001",
43
+ taskType: TaskType.SEMANTIC_SIMILARITY,
44
+ });
45
+ }
46
+
47
+ if (/^https?:\/\//i.test(source)) {
48
+ return new OllamaEmbeddings({
49
+ model: model ?? "embeddinggemma",
50
+ baseUrl: source,
51
+ });
52
+ }
53
+
54
+ const message = `Unsupported embedding source: ${source || "(empty)"}`;
55
+ log(message);
56
+ throw new Error(message);
57
+ }
58
+ }
@@ -0,0 +1,102 @@
1
+ import { DryConfig } from "../types";
2
+ import { configStore } from "../config/configStore";
3
+ import { DryScanServiceDeps } from "./types";
4
+ import { IndexUnitType } from "../types";
5
+ import { minimatch } from "minimatch";
6
+ import { ParsedPairKey } from "./PairingService";
7
+
8
+ export class ExclusionService {
9
+ private config?: DryConfig;
10
+
11
+ constructor(private readonly deps: DryScanServiceDeps) {}
12
+
13
+ async cleanupExcludedFiles(): Promise<void> {
14
+ const config = await this.loadConfig();
15
+ if (!config.excludedPaths || config.excludedPaths.length === 0) return;
16
+
17
+ const units = await this.deps.db.getAllUnits();
18
+ const files = await this.deps.db.getAllFiles();
19
+
20
+ const unitPathsToRemove = new Set<string>();
21
+ for (const unit of units) {
22
+ if (this.pathExcluded(unit.filePath)) {
23
+ unitPathsToRemove.add(unit.filePath);
24
+ }
25
+ }
26
+
27
+ const filePathsToRemove = new Set<string>();
28
+ for (const file of files) {
29
+ if (this.pathExcluded(file.filePath)) {
30
+ filePathsToRemove.add(file.filePath);
31
+ }
32
+ }
33
+
34
+ const paths = [...new Set([...unitPathsToRemove, ...filePathsToRemove])];
35
+ if (paths.length > 0) {
36
+ await this.deps.db.removeUnitsByFilePaths(paths);
37
+ await this.deps.db.removeFilesByFilePaths(paths);
38
+ }
39
+ }
40
+
41
+ async cleanExclusions(): Promise<{ removed: number; kept: number }> {
42
+ const config = await this.loadConfig();
43
+ const units = await this.deps.db.getAllUnits();
44
+
45
+ const actualPairsByType = {
46
+ [IndexUnitType.CLASS]: this.buildPairKeys(units, IndexUnitType.CLASS),
47
+ [IndexUnitType.FUNCTION]: this.buildPairKeys(units, IndexUnitType.FUNCTION),
48
+ [IndexUnitType.BLOCK]: this.buildPairKeys(units, IndexUnitType.BLOCK),
49
+ };
50
+
51
+ const kept: string[] = [];
52
+ const removed: string[] = [];
53
+
54
+ for (const entry of config.excludedPairs || []) {
55
+ const parsed = this.deps.pairing.parsePairKey(entry);
56
+ if (!parsed) {
57
+ removed.push(entry);
58
+ continue;
59
+ }
60
+
61
+ const candidates = actualPairsByType[parsed.type];
62
+ const matched = candidates.some((actual) => this.deps.pairing.pairKeyMatches(actual, parsed));
63
+ if (matched) {
64
+ kept.push(entry);
65
+ } else {
66
+ removed.push(entry);
67
+ }
68
+ }
69
+
70
+ const nextConfig: DryConfig = { ...config, excludedPairs: kept };
71
+ await configStore.save(this.deps.repoPath, nextConfig);
72
+ this.config = nextConfig;
73
+
74
+ return { removed: removed.length, kept: kept.length };
75
+ }
76
+
77
+ private pathExcluded(filePath: string): boolean {
78
+ const config = this.config;
79
+ if (!config || !config.excludedPaths || config.excludedPaths.length === 0) return false;
80
+ return config.excludedPaths.some((pattern) => minimatch(filePath, pattern, { dot: true }));
81
+ }
82
+
83
+ private buildPairKeys(units: any[], type: IndexUnitType): ParsedPairKey[] {
84
+ const typed = units.filter((u) => u.unitType === type);
85
+ const pairs: ParsedPairKey[] = [];
86
+ for (let i = 0; i < typed.length; i++) {
87
+ for (let j = i + 1; j < typed.length; j++) {
88
+ const key = this.deps.pairing.pairKeyForUnits(typed[i], typed[j]);
89
+ const parsed = key ? this.deps.pairing.parsePairKey(key) : null;
90
+ if (parsed) {
91
+ pairs.push(parsed);
92
+ }
93
+ }
94
+ }
95
+ return pairs;
96
+ }
97
+
98
+ private async loadConfig(): Promise<DryConfig> {
99
+ this.config = await configStore.get(this.deps.repoPath);
100
+ return this.config;
101
+ }
102
+ }
@@ -0,0 +1,145 @@
1
+ import crypto from "node:crypto";
2
+ import debug from "debug";
3
+ import { minimatch } from "minimatch";
4
+ import { LanguageExtractor } from "../extractors/LanguageExtractor";
5
+ import { IndexUnitExtractor } from "../IndexUnitExtractor";
6
+ import { IndexUnit, IndexUnitType } from "../types";
7
+ import { BLOCK_HASH_ALGO } from "../const";
8
+
9
+ const log = debug("DryScan:pairs");
10
+
11
+ type UnitLike = Pick<IndexUnit, "unitType" | "filePath" | "name" | "code">;
12
+
13
+ export interface ParsedPairKey {
14
+ type: IndexUnitType;
15
+ left: string;
16
+ right: string;
17
+ key: string;
18
+ }
19
+
20
+ /**
21
+ * Service for building and parsing pair keys with extractor-aware labeling.
22
+ */
23
+ export class PairingService {
24
+ constructor(private readonly indexUnitExtractor: IndexUnitExtractor) {}
25
+
26
+ /**
27
+ * Creates a stable, order-independent key for two units of the same type.
28
+ * Returns null when units differ in type so callers can skip invalid pairs.
29
+ */
30
+ pairKeyForUnits(left: UnitLike, right: UnitLike): string | null {
31
+ if (left.unitType !== right.unitType) {
32
+ log("Skipping pair with mismatched types: %s vs %s", left.unitType, right.unitType);
33
+ return null;
34
+ }
35
+ const type = left.unitType;
36
+ const leftLabel = this.unitLabel(left);
37
+ const rightLabel = this.unitLabel(right);
38
+ const [a, b] = [leftLabel, rightLabel].sort();
39
+ return `${type}|${a}|${b}`;
40
+ }
41
+
42
+ /**
43
+ * Parses a raw pair key into its components, returning null for malformed values.
44
+ * Sorting is applied so callers can compare pairs without worrying about order.
45
+ */
46
+ parsePairKey(value: string): ParsedPairKey | null {
47
+ const parts = value.split("|");
48
+ if (parts.length !== 3) {
49
+ log("Invalid pair key format: %s", value);
50
+ return null;
51
+ }
52
+ const [typeRaw, leftRaw, rightRaw] = parts;
53
+ const type = this.stringToUnitType(typeRaw);
54
+ if (!type) {
55
+ log("Unknown unit type in pair key: %s", typeRaw);
56
+ return null;
57
+ }
58
+ const [left, right] = [leftRaw, rightRaw].sort();
59
+ return { type, left, right, key: `${type}|${left}|${right}` };
60
+ }
61
+
62
+ /**
63
+ * Checks whether an actual pair key satisfies a pattern, with glob matching for class paths.
64
+ */
65
+ pairKeyMatches(actual: ParsedPairKey, pattern: ParsedPairKey): boolean {
66
+ if (actual.type !== pattern.type) return false;
67
+ if (actual.type === IndexUnitType.CLASS) {
68
+ // Allow glob matching for class file paths.
69
+ const forward =
70
+ minimatch(actual.left, pattern.left, { dot: true }) &&
71
+ minimatch(actual.right, pattern.right, { dot: true });
72
+ const swapped =
73
+ minimatch(actual.left, pattern.right, { dot: true }) &&
74
+ minimatch(actual.right, pattern.left, { dot: true });
75
+ return forward || swapped;
76
+ }
77
+
78
+ // Functions and blocks use exact matching on canonical strings.
79
+ return (
80
+ (actual.left === pattern.left && actual.right === pattern.right) ||
81
+ (actual.left === pattern.right && actual.right === pattern.left)
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Derives a reversible, extractor-aware label for a unit.
87
+ * Extractors may override; fallback uses a fixed format per unit type.
88
+ */
89
+ unitLabel(unit: UnitLike): string {
90
+ const extractor = this.findExtractor(unit.filePath);
91
+ const customLabel = extractor?.unitLabel?.(unit as IndexUnit);
92
+ if (customLabel) return customLabel;
93
+
94
+ switch (unit.unitType) {
95
+ case IndexUnitType.CLASS:
96
+ return unit.filePath;
97
+ case IndexUnitType.FUNCTION:
98
+ return this.canonicalFunctionSignature(unit);
99
+ case IndexUnitType.BLOCK:
100
+ return this.normalizedBlockHash(unit);
101
+ default:
102
+ return unit.name;
103
+ }
104
+ }
105
+
106
+ private findExtractor(filePath: string): LanguageExtractor | undefined {
107
+ return this.indexUnitExtractor.extractors.find((ex) => ex.supports(filePath));
108
+ }
109
+
110
+ private canonicalFunctionSignature(unit: UnitLike): string {
111
+ const arity = this.extractArity(unit.code);
112
+ return `${unit.name}(arity:${arity})`;
113
+ }
114
+
115
+ /**
116
+ * Normalizes block code (strips comments/whitespace) and hashes it for pair matching.
117
+ */
118
+ private normalizedBlockHash(unit: UnitLike): string {
119
+ const normalized = this.normalizeCode(unit.code);
120
+ return crypto.createHash(BLOCK_HASH_ALGO).update(normalized).digest("hex");
121
+ }
122
+
123
+ private stringToUnitType(value: string): IndexUnitType | null {
124
+ if (value === IndexUnitType.CLASS) return IndexUnitType.CLASS;
125
+ if (value === IndexUnitType.FUNCTION) return IndexUnitType.FUNCTION;
126
+ if (value === IndexUnitType.BLOCK) return IndexUnitType.BLOCK;
127
+ return null;
128
+ }
129
+
130
+ private extractArity(code: string): number {
131
+ const match = code.match(/^[^{]*?\(([^)]*)\)/s);
132
+ if (!match) return 0;
133
+ const params = match[1]
134
+ .split(",")
135
+ .map((p) => p.trim())
136
+ .filter(Boolean);
137
+ return params.length;
138
+ }
139
+
140
+ private normalizeCode(code: string): string {
141
+ const withoutBlockComments = code.replace(/\/\*[\s\S]*?\*\//g, "");
142
+ const withoutLineComments = withoutBlockComments.replace(/\/\/[^\n\r]*/g, "");
143
+ return withoutLineComments.replace(/\s+/g, "");
144
+ }
145
+ }
@@ -0,0 +1,93 @@
1
+ import path from "path";
2
+ import fs from "fs/promises";
3
+ import { DryScanServiceDeps } from "./types";
4
+ import { ExclusionService } from "./ExclusionService";
5
+ import { IndexUnit } from "../types";
6
+ import { EmbeddingService } from "./EmbeddingService";
7
+ import { FileEntity } from "../db/entities/FileEntity";
8
+ import { IndexUnitExtractor } from "../IndexUnitExtractor";
9
+
10
+ export interface InitOptions {
11
+ skipEmbeddings?: boolean;
12
+ }
13
+
14
+ export class RepositoryInitializer {
15
+ constructor(
16
+ private readonly deps: DryScanServiceDeps,
17
+ private readonly exclusionService: ExclusionService
18
+ ) {}
19
+
20
+ async init(options?: InitOptions): Promise<void> {
21
+ const extractor = this.deps.extractor;
22
+
23
+ console.log("[DryScan] Phase 1/3: Extracting code units...");
24
+ await this.initUnits(extractor);
25
+ console.log("[DryScan] Phase 2/3: Computing embeddings (may be slow)...");
26
+ await this.computeEmbeddings(options?.skipEmbeddings === true);
27
+ console.log("[DryScan] Phase 3/3: Tracking files...");
28
+ await this.trackFiles(extractor);
29
+ await this.exclusionService.cleanupExcludedFiles();
30
+ console.log("[DryScan] Initialization phases complete.");
31
+ }
32
+
33
+ private async initUnits(extractor: IndexUnitExtractor): Promise<void> {
34
+ const units = await extractor.scan(this.deps.repoPath);
35
+ console.log(`[DryScan] Extracted ${units.length} index units.`);
36
+ await this.deps.db.saveUnits(units);
37
+ }
38
+
39
+ private async computeEmbeddings(skipEmbeddings: boolean): Promise<void> {
40
+ if (skipEmbeddings) {
41
+ console.log("[DryScan] Skipping embedding computation by request.");
42
+ return;
43
+ }
44
+ const allUnits: IndexUnit[] = await this.deps.db.getAllUnits();
45
+ const total = allUnits.length;
46
+ console.log(`[DryScan] Computing embeddings for ${total} units...`);
47
+
48
+ const updated: IndexUnit[] = [];
49
+ const progressInterval = Math.max(1, Math.ceil(total / 10));
50
+ const embeddingService = new EmbeddingService(this.deps.repoPath);
51
+
52
+ for (let i = 0; i < total; i++) {
53
+ const unit = allUnits[i];
54
+ try {
55
+ const enriched = await embeddingService.addEmbedding(unit);
56
+ updated.push(enriched);
57
+ } catch (err: any) {
58
+ console.error(
59
+ `[DryScan] Embedding failed for ${unit.filePath} (${unit.name}): ${err?.message || err}`
60
+ );
61
+ throw err;
62
+ }
63
+
64
+ const completed = i + 1;
65
+ if (completed === total || completed % progressInterval === 0) {
66
+ const pct = Math.floor((completed / total) * 100);
67
+ console.log(`[DryScan] Embeddings ${completed}/${total} (${pct}%)`);
68
+ }
69
+ }
70
+
71
+ await this.deps.db.updateUnits(updated);
72
+ }
73
+
74
+ private async trackFiles(extractor: IndexUnitExtractor): Promise<void> {
75
+ const allFunctions = await extractor.listSourceFiles(this.deps.repoPath);
76
+ const fileEntities: FileEntity[] = [];
77
+
78
+ for (const relPath of allFunctions) {
79
+ const fullPath = path.join(this.deps.repoPath, relPath);
80
+ const stat = await fs.stat(fullPath);
81
+ const checksum = await extractor.computeChecksum(fullPath);
82
+
83
+ const fileEntity = new FileEntity();
84
+ fileEntity.filePath = relPath;
85
+ fileEntity.checksum = checksum;
86
+ fileEntity.mtime = stat.mtimeMs;
87
+ fileEntities.push(fileEntity);
88
+ }
89
+
90
+ await this.deps.db.saveFiles(fileEntities);
91
+ console.log(`[DryScan] Tracked ${fileEntities.length} files.`);
92
+ }
93
+ }
@@ -0,0 +1,28 @@
1
+ import debug from "debug";
2
+ import { DryScanServiceDeps } from "./types";
3
+ import { ExclusionService } from "./ExclusionService";
4
+ import { performIncrementalUpdate } from "../DryScanUpdater";
5
+ import { DuplicationCache } from "./DuplicationCache";
6
+
7
+ const log = debug("DryScan:UpdateService");
8
+
9
+ export class UpdateService {
10
+ constructor(
11
+ private readonly deps: DryScanServiceDeps,
12
+ private readonly exclusionService: ExclusionService
13
+ ) {}
14
+
15
+ async updateIndex(): Promise<void> {
16
+ const extractor = this.deps.extractor;
17
+ const cache = DuplicationCache.getInstance();
18
+
19
+ try {
20
+ const changeSet = await performIncrementalUpdate(this.deps.repoPath, extractor, this.deps.db);
21
+ await this.exclusionService.cleanupExcludedFiles();
22
+ await cache.invalidate([...changeSet.changed, ...changeSet.deleted]);
23
+ } catch (err) {
24
+ log("Error during index update:", err);
25
+ throw err;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,10 @@
1
+ import { DryScanDatabase } from "../db/DryScanDatabase";
2
+ import { IndexUnitExtractor } from "../IndexUnitExtractor";
3
+ import { PairingService } from "./PairingService";
4
+
5
+ export interface DryScanServiceDeps {
6
+ repoPath: string;
7
+ db: DryScanDatabase;
8
+ extractor: IndexUnitExtractor;
9
+ pairing: PairingService;
10
+ }
@@ -0,0 +1,7 @@
1
+ declare module "glob-gitignore" {
2
+ import type { GlobOptions } from "glob";
3
+ export function glob(patterns: string | string[], options?: GlobOptions & { ignore?: string | string[] }): Promise<string[]>;
4
+ export function sync(patterns: string | string[], options?: GlobOptions & { ignore?: string | string[] }): string[];
5
+ export function hasMagic(patterns: string | string[], options?: GlobOptions): boolean;
6
+ export default glob;
7
+ }
@@ -0,0 +1,7 @@
1
+ declare module "short-uuid" {
2
+ export function generate(): string;
3
+ const shortUuid: {
4
+ generate: () => string;
5
+ };
6
+ export default shortUuid;
7
+ }
@@ -0,0 +1,4 @@
1
+ declare module "tree-sitter-java" {
2
+ const Java: any;
3
+ export = Java;
4
+ }
package/src/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ export enum IndexUnitType {
2
+ CLASS = "class",
3
+ FUNCTION = "function",
4
+ BLOCK = "block",
5
+ }
6
+
7
+ export interface DuplicateGroup {
8
+ id: string;
9
+ similarity: number;
10
+ left: DuplicateSide;
11
+ right: DuplicateSide;
12
+ shortId: string;
13
+ exclusionString: string;
14
+ }
15
+
16
+ export interface DuplicationScore {
17
+ score: number;
18
+ grade: 'Excellent' | 'Good' | 'Fair' | 'Poor' | 'Critical';
19
+ totalLines: number;
20
+ duplicateLines: number;
21
+ duplicateGroups: number;
22
+ }
23
+
24
+ export interface DuplicateAnalysisResult {
25
+ duplicates: DuplicateGroup[];
26
+ score: DuplicationScore;
27
+ }
28
+
29
+ export interface DuplicateReport {
30
+ version: number;
31
+ generatedAt: string;
32
+ threshold: number;
33
+ score: DuplicationScore;
34
+ duplicates: DuplicateGroup[];
35
+ }
36
+
37
+ export interface DuplicateSide {
38
+ id: string;
39
+ name: string;
40
+ filePath: string;
41
+ startLine: number;
42
+ endLine: number;
43
+ code: string;
44
+ unitType: IndexUnitType;
45
+ }
46
+
47
+ export interface DryConfig {
48
+ excludedPaths: string[];
49
+ excludedPairs: string[];
50
+ minLines: number;
51
+ minBlockLines: number;
52
+ threshold: number;
53
+ embeddingModel: string;
54
+ embeddingSource?: string;
55
+ contextLength: number;
56
+ }
57
+
58
+ export interface IndexUnit {
59
+ id: string;
60
+ name: string;
61
+ filePath: string;
62
+ startLine: number;
63
+ endLine: number;
64
+ code: string;
65
+ unitType: IndexUnitType;
66
+ parentId?: string | null;
67
+ parent?: IndexUnit | null;
68
+ children?: IndexUnit[];
69
+ embedding?: number[] | null;
70
+ }
71
+
72
+ export interface EmbeddingResult {
73
+ processed: number;
74
+ updated: number;
75
+ errors: string[];
76
+ }