@saptools/service-flow 0.1.37 → 0.1.38
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.
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
- package/src/cli.ts +865 -0
- package/src/config/defaults.ts +14 -0
- package/src/config/workspace-config.ts +61 -0
- package/src/db/connection.ts +131 -0
- package/src/db/migrations.ts +81 -0
- package/src/db/repositories.ts +353 -0
- package/src/db/schema.ts +24 -0
- package/src/discovery/classify-repository.ts +50 -0
- package/src/discovery/discover-repositories.ts +58 -0
- package/src/index.ts +13 -0
- package/src/indexer/incremental-index.ts +25 -0
- package/src/indexer/repository-indexer.ts +137 -0
- package/src/indexer/workspace-indexer.ts +29 -0
- package/src/linker/cross-repo-linker.ts +406 -0
- package/src/linker/dynamic-edge-resolver.ts +45 -0
- package/src/linker/external-http-target.ts +38 -0
- package/src/linker/helper-package-linker.ts +57 -0
- package/src/linker/odata-path-normalizer.ts +236 -0
- package/src/linker/operation-decorator-normalizer.ts +47 -0
- package/src/linker/remote-query-target.ts +39 -0
- package/src/linker/service-resolver.ts +223 -0
- package/src/output/json-output.ts +7 -0
- package/src/output/mermaid-output.ts +16 -0
- package/src/output/table-output.ts +21 -0
- package/src/parsers/cds-parser.ts +147 -0
- package/src/parsers/decorator-parser.ts +76 -0
- package/src/parsers/generated-constants-parser.ts +23 -0
- package/src/parsers/handler-registration-parser.ts +177 -0
- package/src/parsers/outbound-call-parser.ts +398 -0
- package/src/parsers/package-json-parser.ts +63 -0
- package/src/parsers/service-binding-parser.ts +826 -0
- package/src/parsers/symbol-parser.ts +328 -0
- package/src/parsers/ts-project.ts +13 -0
- package/src/trace/selectors.ts +20 -0
- package/src/trace/trace-engine.ts +647 -0
- package/src/trace/traversal.ts +4 -0
- package/src/types.ts +186 -0
- package/src/utils/diagnostics.ts +10 -0
- package/src/utils/hashing.ts +10 -0
- package/src/utils/path-utils.ts +13 -0
- package/src/utils/redaction.ts +20 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DEFAULT_IGNORES = [
|
|
2
|
+
'node_modules',
|
|
3
|
+
'gen',
|
|
4
|
+
'dist',
|
|
5
|
+
'coverage',
|
|
6
|
+
'.git',
|
|
7
|
+
'.turbo',
|
|
8
|
+
'.next',
|
|
9
|
+
'.cache',
|
|
10
|
+
'.service-flow'
|
|
11
|
+
] as const;
|
|
12
|
+
export const CONFIG_DIR = '.service-flow';
|
|
13
|
+
export const CONFIG_FILE = 'config.json';
|
|
14
|
+
export const DEFAULT_DB_FILE = 'service-flow.db';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import {
|
|
5
|
+
CONFIG_DIR,
|
|
6
|
+
CONFIG_FILE,
|
|
7
|
+
DEFAULT_DB_FILE,
|
|
8
|
+
DEFAULT_IGNORES,
|
|
9
|
+
} from './defaults.js';
|
|
10
|
+
const schema = z.object({
|
|
11
|
+
rootPath: z.string(),
|
|
12
|
+
dbPath: z.string(),
|
|
13
|
+
ignore: z.array(z.string()),
|
|
14
|
+
createdAt: z.string(),
|
|
15
|
+
updatedAt: z.string(),
|
|
16
|
+
});
|
|
17
|
+
export type WorkspaceConfig = z.infer<typeof schema>;
|
|
18
|
+
export function configPath(rootPath: string): string {
|
|
19
|
+
return path.join(rootPath, CONFIG_DIR, CONFIG_FILE);
|
|
20
|
+
}
|
|
21
|
+
export function defaultDbPath(rootPath: string): string {
|
|
22
|
+
return path.join(rootPath, CONFIG_DIR, DEFAULT_DB_FILE);
|
|
23
|
+
}
|
|
24
|
+
export async function saveWorkspaceConfig(
|
|
25
|
+
config: WorkspaceConfig,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
await fs.mkdir(path.dirname(configPath(config.rootPath)), {
|
|
28
|
+
recursive: true,
|
|
29
|
+
});
|
|
30
|
+
await fs.writeFile(
|
|
31
|
+
configPath(config.rootPath),
|
|
32
|
+
`${JSON.stringify(config, null, 2)}\n`,
|
|
33
|
+
);
|
|
34
|
+
if (path.dirname(config.dbPath) === path.dirname(configPath(config.rootPath)))
|
|
35
|
+
await fs.writeFile(
|
|
36
|
+
path.join(path.dirname(config.dbPath), '.service-flow-state'),
|
|
37
|
+
'service-flow\n',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
export async function loadWorkspaceConfig(
|
|
41
|
+
workspace?: string,
|
|
42
|
+
): Promise<WorkspaceConfig> {
|
|
43
|
+
const root = path.resolve(workspace ?? process.cwd());
|
|
44
|
+
const data = await fs.readFile(configPath(root), 'utf8');
|
|
45
|
+
return schema.parse(JSON.parse(data) as unknown);
|
|
46
|
+
}
|
|
47
|
+
export function createWorkspaceConfig(
|
|
48
|
+
rootPath: string,
|
|
49
|
+
dbPath?: string,
|
|
50
|
+
ignore: string[] = [...DEFAULT_IGNORES],
|
|
51
|
+
): WorkspaceConfig {
|
|
52
|
+
const now = new Date().toISOString();
|
|
53
|
+
const root = path.resolve(rootPath);
|
|
54
|
+
return {
|
|
55
|
+
rootPath: root,
|
|
56
|
+
dbPath: path.resolve(dbPath ?? defaultDbPath(root)),
|
|
57
|
+
ignore,
|
|
58
|
+
createdAt: now,
|
|
59
|
+
updatedAt: now,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { migrate } from './migrations.js';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
5
|
+
|
|
6
|
+
type SqlValue = string | number | bigint | Buffer | null | undefined;
|
|
7
|
+
interface NativeStatement {
|
|
8
|
+
run: (...params: SqlValue[]) => { changes: number };
|
|
9
|
+
get: (...params: SqlValue[]) => Record<string, unknown> | undefined;
|
|
10
|
+
all: (...params: SqlValue[]) => Array<Record<string, unknown>>;
|
|
11
|
+
}
|
|
12
|
+
interface NativeDatabase {
|
|
13
|
+
exec: (sql: string) => void;
|
|
14
|
+
prepare: (sql: string) => NativeStatement;
|
|
15
|
+
close: () => void;
|
|
16
|
+
}
|
|
17
|
+
interface NodeSqliteModule {
|
|
18
|
+
DatabaseSync: new (location: string, options?: { open?: boolean; readOnly?: boolean }) => NativeDatabase;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let sqliteWarningFilterInstalled = false;
|
|
22
|
+
function installSqliteWarningFilter(): void {
|
|
23
|
+
if (sqliteWarningFilterInstalled) return;
|
|
24
|
+
sqliteWarningFilterInstalled = true;
|
|
25
|
+
const original = process.emitWarning.bind(process);
|
|
26
|
+
process.emitWarning = ((warning: string | Error, ...args: unknown[]): void => {
|
|
27
|
+
const text = warning instanceof Error ? warning.message : String(warning);
|
|
28
|
+
if (text.includes('SQLite is an experimental feature')) return;
|
|
29
|
+
Reflect.apply(original, process, [warning, ...args]);
|
|
30
|
+
}) as typeof process.emitWarning;
|
|
31
|
+
}
|
|
32
|
+
export interface Statement {
|
|
33
|
+
run: (...params: unknown[]) => { changes: number };
|
|
34
|
+
get: (...params: unknown[]) => Record<string, unknown> | undefined;
|
|
35
|
+
all: (...params: unknown[]) => Array<Record<string, unknown>>;
|
|
36
|
+
}
|
|
37
|
+
export interface Db {
|
|
38
|
+
path: string;
|
|
39
|
+
readonly: boolean;
|
|
40
|
+
exec: (sql: string) => void;
|
|
41
|
+
prepare: (sql: string) => Statement;
|
|
42
|
+
pragma: (sql: string) => Array<Record<string, unknown>>;
|
|
43
|
+
transaction: <T>(fn: () => T) => T;
|
|
44
|
+
close: () => void;
|
|
45
|
+
}
|
|
46
|
+
export interface OpenDatabaseOptions {
|
|
47
|
+
readonly?: boolean;
|
|
48
|
+
migrate?: boolean;
|
|
49
|
+
}
|
|
50
|
+
function loadSqlite(): NodeSqliteModule {
|
|
51
|
+
try {
|
|
52
|
+
installSqliteWarningFilter();
|
|
53
|
+
const moduleValue = process.getBuiltinModule('node:sqlite') as unknown;
|
|
54
|
+
if (!moduleValue || typeof moduleValue !== 'object' || !('DatabaseSync' in moduleValue))
|
|
55
|
+
throw new Error('node:sqlite DatabaseSync is unavailable');
|
|
56
|
+
const sqlite = moduleValue as NodeSqliteModule;
|
|
57
|
+
if (typeof sqlite.DatabaseSync !== 'function')
|
|
58
|
+
throw new Error('node:sqlite DatabaseSync is not a constructor');
|
|
59
|
+
return sqlite;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`service-flow ${VERSION} requires Node.js >=24 with node:sqlite DatabaseSync support. Upgrade Node.js or install a service-flow build with a compatible SQLite driver.`,
|
|
63
|
+
{ cause: error },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function bindParams(params: unknown[]): SqlValue[] {
|
|
68
|
+
return params.map((param) => {
|
|
69
|
+
if (param === undefined || param === null) return null;
|
|
70
|
+
if (typeof param === 'string' || typeof param === 'number' || typeof param === 'bigint' || Buffer.isBuffer(param)) return param;
|
|
71
|
+
if (typeof param === 'boolean') return param ? 1 : 0;
|
|
72
|
+
return JSON.stringify(param);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function openDatabase(dbPath: string, options: OpenDatabaseOptions = {}): Db {
|
|
76
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
77
|
+
const sqlite = loadSqlite();
|
|
78
|
+
const native = new sqlite.DatabaseSync(dbPath, { readOnly: Boolean(options.readonly) });
|
|
79
|
+
let inTransaction = false;
|
|
80
|
+
const db: Db = {
|
|
81
|
+
path: dbPath,
|
|
82
|
+
readonly: Boolean(options.readonly),
|
|
83
|
+
exec(sql: string): void {
|
|
84
|
+
native.exec(sql);
|
|
85
|
+
},
|
|
86
|
+
prepare(sql: string): Statement {
|
|
87
|
+
const statement = native.prepare(sql);
|
|
88
|
+
return {
|
|
89
|
+
run: (...params: unknown[]) => statement.run(...bindParams(params)),
|
|
90
|
+
get: (...params: unknown[]) => statement.get(...bindParams(params)),
|
|
91
|
+
all: (...params: unknown[]) => statement.all(...bindParams(params)),
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
pragma(sql: string): Array<Record<string, unknown>> {
|
|
95
|
+
const normalized = sql.trim().replace(/;$/, '');
|
|
96
|
+
if (/=/.test(normalized)) {
|
|
97
|
+
native.exec(`PRAGMA ${normalized}`);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
return native.prepare(`PRAGMA ${normalized}`).all();
|
|
101
|
+
},
|
|
102
|
+
transaction<T>(fn: () => T): T {
|
|
103
|
+
if (inTransaction) return fn();
|
|
104
|
+
inTransaction = true;
|
|
105
|
+
native.exec('BEGIN IMMEDIATE');
|
|
106
|
+
try {
|
|
107
|
+
const result = fn();
|
|
108
|
+
native.exec('COMMIT');
|
|
109
|
+
return result;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
native.exec('ROLLBACK');
|
|
112
|
+
throw error;
|
|
113
|
+
} finally {
|
|
114
|
+
inTransaction = false;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
close(): void {
|
|
118
|
+
native.close();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
db.pragma('busy_timeout = 10000');
|
|
122
|
+
db.pragma('foreign_keys = ON');
|
|
123
|
+
if (!options.readonly) {
|
|
124
|
+
db.pragma('journal_mode = WAL');
|
|
125
|
+
if (options.migrate !== false) migrate(db);
|
|
126
|
+
}
|
|
127
|
+
return db;
|
|
128
|
+
}
|
|
129
|
+
export function openReadOnlyDatabase(dbPath: string): Db {
|
|
130
|
+
return openDatabase(dbPath, { readonly: true, migrate: false });
|
|
131
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
import { schemaSql } from './schema.js';
|
|
3
|
+
const CURRENT_SCHEMA_VERSION = 8;
|
|
4
|
+
const columns: Record<string, Array<{ name: string; ddl: string }>> = {
|
|
5
|
+
service_bindings: [
|
|
6
|
+
{ name: 'helper_chain_json', ddl: 'ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT' },
|
|
7
|
+
{ name: 'alias_expr', ddl: 'ALTER TABLE service_bindings ADD COLUMN alias_expr TEXT' },
|
|
8
|
+
],
|
|
9
|
+
repositories: [
|
|
10
|
+
{ name: 'fingerprint', ddl: 'ALTER TABLE repositories ADD COLUMN fingerprint TEXT' },
|
|
11
|
+
{ name: 'fact_generation', ddl: 'ALTER TABLE repositories ADD COLUMN fact_generation INTEGER NOT NULL DEFAULT 0' },
|
|
12
|
+
{ name: 'graph_generation', ddl: 'ALTER TABLE repositories ADD COLUMN graph_generation INTEGER NOT NULL DEFAULT 0' },
|
|
13
|
+
{ name: 'graph_stale_reason', ddl: 'ALTER TABLE repositories ADD COLUMN graph_stale_reason TEXT' },
|
|
14
|
+
{ name: 'graph_stale_at', ddl: 'ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT' },
|
|
15
|
+
{ name: 'fact_analyzer_version', ddl: "ALTER TABLE repositories ADD COLUMN fact_analyzer_version TEXT DEFAULT 'legacy'" },
|
|
16
|
+
],
|
|
17
|
+
graph_edges: [
|
|
18
|
+
{ name: 'status', ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
|
|
19
|
+
{ name: 'generation', ddl: 'ALTER TABLE graph_edges ADD COLUMN generation INTEGER NOT NULL DEFAULT 0' },
|
|
20
|
+
],
|
|
21
|
+
handler_registrations: [
|
|
22
|
+
{ name: 'class_name', ddl: 'ALTER TABLE handler_registrations ADD COLUMN class_name TEXT' },
|
|
23
|
+
{ name: 'import_source', ddl: 'ALTER TABLE handler_registrations ADD COLUMN import_source TEXT' },
|
|
24
|
+
],
|
|
25
|
+
symbols: [
|
|
26
|
+
{ name: 'start_offset', ddl: 'ALTER TABLE symbols ADD COLUMN start_offset INTEGER' },
|
|
27
|
+
{ name: 'end_offset', ddl: 'ALTER TABLE symbols ADD COLUMN end_offset INTEGER' },
|
|
28
|
+
{ name: 'source_file', ddl: 'ALTER TABLE symbols ADD COLUMN source_file TEXT' },
|
|
29
|
+
{ name: 'exported_name', ddl: 'ALTER TABLE symbols ADD COLUMN exported_name TEXT' },
|
|
30
|
+
{ name: 'evidence_json', ddl: 'ALTER TABLE symbols ADD COLUMN evidence_json TEXT' },
|
|
31
|
+
],
|
|
32
|
+
outbound_calls: [
|
|
33
|
+
{ name: 'local_service_name', ddl: 'ALTER TABLE outbound_calls ADD COLUMN local_service_name TEXT' },
|
|
34
|
+
{ name: 'local_service_lookup', ddl: 'ALTER TABLE outbound_calls ADD COLUMN local_service_lookup TEXT' },
|
|
35
|
+
{ name: 'alias_chain_json', ddl: 'ALTER TABLE outbound_calls ADD COLUMN alias_chain_json TEXT' },
|
|
36
|
+
{ name: 'evidence_json', ddl: 'ALTER TABLE outbound_calls ADD COLUMN evidence_json TEXT' },
|
|
37
|
+
{ name: 'external_target_kind', ddl: 'ALTER TABLE outbound_calls ADD COLUMN external_target_kind TEXT' },
|
|
38
|
+
{ name: 'external_target_id', ddl: 'ALTER TABLE outbound_calls ADD COLUMN external_target_id TEXT' },
|
|
39
|
+
{ name: 'external_target_label', ddl: 'ALTER TABLE outbound_calls ADD COLUMN external_target_label TEXT' },
|
|
40
|
+
{ name: 'external_target_dynamic', ddl: 'ALTER TABLE outbound_calls ADD COLUMN external_target_dynamic INTEGER NOT NULL DEFAULT 0' },
|
|
41
|
+
],
|
|
42
|
+
index_runs: [
|
|
43
|
+
{ name: 'error_message', ddl: 'ALTER TABLE index_runs ADD COLUMN error_message TEXT' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
function hasColumn(db: Db, table: string, column: string): boolean {
|
|
47
|
+
return (db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: string }>).some((row) => row.name === column);
|
|
48
|
+
}
|
|
49
|
+
function userVersion(db: Db): number {
|
|
50
|
+
const row = db.pragma('user_version')[0] as { user_version?: number } | undefined;
|
|
51
|
+
return Number(row?.user_version ?? 0);
|
|
52
|
+
}
|
|
53
|
+
function addMissingColumns(db: Db): void {
|
|
54
|
+
for (const [table, tableColumns] of Object.entries(columns)) {
|
|
55
|
+
for (const column of tableColumns) {
|
|
56
|
+
if (!hasColumn(db, table, column.name)) db.prepare(column.ddl).run();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function normalizeLegacyStatus(db: Db): void {
|
|
61
|
+
db.prepare("UPDATE graph_edges SET status=CASE WHEN edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' THEN 'resolved' WHEN edge_type IN ('HANDLER_RUNS_DB_QUERY','HANDLER_CALLS_EXTERNAL_HTTP','HANDLER_EMITS_EVENT','EVENT_CONSUMED_BY_HANDLER') THEN 'terminal' WHEN edge_type='DYNAMIC_EDGE_CANDIDATE' THEN 'dynamic' WHEN status='ambiguous' THEN 'ambiguous' ELSE status END").run();
|
|
62
|
+
db.prepare("UPDATE repositories SET graph_stale_reason='schema_migration_requires_relink', graph_stale_at=COALESCE(graph_stale_at, datetime('now')) WHERE EXISTS (SELECT 1 FROM graph_edges WHERE graph_edges.workspace_id=repositories.workspace_id) AND graph_generation=0").run();
|
|
63
|
+
}
|
|
64
|
+
export function migrate(db: Db): void {
|
|
65
|
+
db.transaction(() => {
|
|
66
|
+
const version = userVersion(db);
|
|
67
|
+
if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
|
|
68
|
+
db.exec(schemaSql);
|
|
69
|
+
addMissingColumns(db);
|
|
70
|
+
normalizeLegacyStatus(db);
|
|
71
|
+
const violations = db.pragma('foreign_key_check');
|
|
72
|
+
if (violations.length > 0) throw new Error('SQLite foreign_key_check failed during migration');
|
|
73
|
+
db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function schemaVersion(db: Db): number {
|
|
77
|
+
return userVersion(db);
|
|
78
|
+
}
|
|
79
|
+
export function foreignKeyViolations(db: Db): Array<Record<string, unknown>> {
|
|
80
|
+
return db.pragma('foreign_key_check');
|
|
81
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
import type {
|
|
3
|
+
CdsRequire,
|
|
4
|
+
CdsServiceFact,
|
|
5
|
+
HandlerClassFact,
|
|
6
|
+
HandlerRegistrationFact,
|
|
7
|
+
OutboundCallFact,
|
|
8
|
+
ServiceBindingFact,
|
|
9
|
+
ExecutableSymbolFact,
|
|
10
|
+
SymbolCallFact,
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
export interface RepoRow {
|
|
13
|
+
id: number;
|
|
14
|
+
name: string;
|
|
15
|
+
absolute_path: string;
|
|
16
|
+
relative_path: string;
|
|
17
|
+
package_name: string | null;
|
|
18
|
+
package_version: string | null;
|
|
19
|
+
dependencies_json: string;
|
|
20
|
+
kind: string;
|
|
21
|
+
fingerprint?: string | null;
|
|
22
|
+
fact_generation?: number;
|
|
23
|
+
graph_generation?: number;
|
|
24
|
+
graph_stale_reason?: string | null;
|
|
25
|
+
fact_analyzer_version?: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface WorkspaceRow {
|
|
28
|
+
id: number;
|
|
29
|
+
root_path: string;
|
|
30
|
+
db_path: string;
|
|
31
|
+
}
|
|
32
|
+
export function upsertWorkspace(
|
|
33
|
+
db: Db,
|
|
34
|
+
rootPath: string,
|
|
35
|
+
dbPath: string,
|
|
36
|
+
): number {
|
|
37
|
+
const now = new Date().toISOString();
|
|
38
|
+
db.prepare(
|
|
39
|
+
'INSERT INTO workspaces(root_path,db_path,created_at,updated_at) VALUES(?,?,?,?) ON CONFLICT(root_path) DO UPDATE SET db_path=excluded.db_path,updated_at=excluded.updated_at',
|
|
40
|
+
).run(rootPath, dbPath, now, now);
|
|
41
|
+
return Number(
|
|
42
|
+
db.prepare('SELECT id FROM workspaces WHERE root_path=?').get(rootPath)?.id,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
export function getWorkspace(
|
|
46
|
+
db: Db,
|
|
47
|
+
rootPath: string,
|
|
48
|
+
): WorkspaceRow | undefined {
|
|
49
|
+
return db
|
|
50
|
+
.prepare('SELECT * FROM workspaces WHERE root_path=?')
|
|
51
|
+
.get(rootPath) as WorkspaceRow | undefined;
|
|
52
|
+
}
|
|
53
|
+
export function upsertRepository(
|
|
54
|
+
db: Db,
|
|
55
|
+
workspaceId: number,
|
|
56
|
+
r: {
|
|
57
|
+
name: string;
|
|
58
|
+
absolutePath: string;
|
|
59
|
+
relativePath: string;
|
|
60
|
+
isGitRepo: boolean;
|
|
61
|
+
packageName?: string;
|
|
62
|
+
packageVersion?: string;
|
|
63
|
+
dependencies?: Record<string, string>;
|
|
64
|
+
kind?: string;
|
|
65
|
+
},
|
|
66
|
+
): number {
|
|
67
|
+
db.prepare(
|
|
68
|
+
`INSERT INTO repositories(workspace_id,name,absolute_path,relative_path,package_name,package_version,dependencies_json,kind,is_git_repo) VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(workspace_id,absolute_path) DO UPDATE SET name=excluded.name,relative_path=excluded.relative_path,package_name=excluded.package_name,package_version=excluded.package_version,dependencies_json=excluded.dependencies_json,kind=excluded.kind`,
|
|
69
|
+
).run(
|
|
70
|
+
workspaceId,
|
|
71
|
+
r.name,
|
|
72
|
+
r.absolutePath,
|
|
73
|
+
r.relativePath,
|
|
74
|
+
r.packageName,
|
|
75
|
+
r.packageVersion,
|
|
76
|
+
JSON.stringify(r.dependencies ?? {}),
|
|
77
|
+
r.kind ?? 'unknown',
|
|
78
|
+
r.isGitRepo ? 1 : 0,
|
|
79
|
+
);
|
|
80
|
+
return Number(
|
|
81
|
+
db
|
|
82
|
+
.prepare(
|
|
83
|
+
'SELECT id FROM repositories WHERE workspace_id=? AND absolute_path=?',
|
|
84
|
+
)
|
|
85
|
+
.get(workspaceId, r.absolutePath)?.id,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
export function listRepositories(db: Db): RepoRow[] {
|
|
89
|
+
return db
|
|
90
|
+
.prepare('SELECT * FROM repositories ORDER BY name')
|
|
91
|
+
.all() as unknown as RepoRow[];
|
|
92
|
+
}
|
|
93
|
+
export function repoByName(db: Db, name: string): RepoRow | undefined {
|
|
94
|
+
return db
|
|
95
|
+
.prepare('SELECT * FROM repositories WHERE name=? OR package_name=?')
|
|
96
|
+
.get(name, name) as RepoRow | undefined;
|
|
97
|
+
}
|
|
98
|
+
export function clearRepoFacts(db: Db, repoId: number): void {
|
|
99
|
+
for (const t of [
|
|
100
|
+
'cds_requires',
|
|
101
|
+
'cds_services',
|
|
102
|
+
'handler_classes',
|
|
103
|
+
'outbound_calls',
|
|
104
|
+
'symbol_calls',
|
|
105
|
+
'handler_registrations',
|
|
106
|
+
'service_bindings',
|
|
107
|
+
'symbols',
|
|
108
|
+
'diagnostics',
|
|
109
|
+
'files',
|
|
110
|
+
])
|
|
111
|
+
db.prepare(`DELETE FROM ${t} WHERE repo_id=?`).run(repoId);
|
|
112
|
+
db.prepare('DELETE FROM search_index WHERE repo=?').run(String(repoId));
|
|
113
|
+
}
|
|
114
|
+
export function insertRequires(
|
|
115
|
+
db: Db,
|
|
116
|
+
repoId: number,
|
|
117
|
+
rows: CdsRequire[],
|
|
118
|
+
): void {
|
|
119
|
+
const stmt = db.prepare(
|
|
120
|
+
'INSERT INTO cds_requires(repo_id,alias,kind,model,destination,service_path,request_timeout,raw_json) VALUES(?,?,?,?,?,?,?,?)',
|
|
121
|
+
);
|
|
122
|
+
for (const r of rows)
|
|
123
|
+
stmt.run(
|
|
124
|
+
repoId,
|
|
125
|
+
r.alias,
|
|
126
|
+
r.kind,
|
|
127
|
+
r.model,
|
|
128
|
+
r.destination,
|
|
129
|
+
r.servicePath,
|
|
130
|
+
r.requestTimeout,
|
|
131
|
+
r.rawJson,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
export function insertService(
|
|
135
|
+
db: Db,
|
|
136
|
+
repoId: number,
|
|
137
|
+
s: CdsServiceFact,
|
|
138
|
+
): number {
|
|
139
|
+
const id = Number(
|
|
140
|
+
db
|
|
141
|
+
.prepare(
|
|
142
|
+
'INSERT INTO cds_services(repo_id,namespace,service_name,qualified_name,service_path,is_extend,source_file,source_line) VALUES(?,?,?,?,?,?,?,?) RETURNING id',
|
|
143
|
+
)
|
|
144
|
+
.get(
|
|
145
|
+
repoId,
|
|
146
|
+
s.namespace,
|
|
147
|
+
s.serviceName,
|
|
148
|
+
s.qualifiedName,
|
|
149
|
+
s.servicePath,
|
|
150
|
+
s.isExtend ? 1 : 0,
|
|
151
|
+
s.sourceFile,
|
|
152
|
+
s.sourceLine,
|
|
153
|
+
)?.id,
|
|
154
|
+
);
|
|
155
|
+
const stmt = db.prepare(
|
|
156
|
+
'INSERT INTO cds_operations(service_id,operation_type,operation_name,operation_path,params_json,return_type,source_file,source_line) VALUES(?,?,?,?,?,?,?,?)',
|
|
157
|
+
);
|
|
158
|
+
db.prepare(
|
|
159
|
+
'INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)',
|
|
160
|
+
).run('service', s.qualifiedName, s.servicePath, String(repoId));
|
|
161
|
+
for (const o of s.operations)
|
|
162
|
+
stmt.run(
|
|
163
|
+
id,
|
|
164
|
+
o.operationType,
|
|
165
|
+
o.operationName,
|
|
166
|
+
o.operationPath,
|
|
167
|
+
o.paramsJson,
|
|
168
|
+
o.returnType,
|
|
169
|
+
o.sourceFile,
|
|
170
|
+
o.sourceLine,
|
|
171
|
+
);
|
|
172
|
+
const search = db.prepare(
|
|
173
|
+
'INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)',
|
|
174
|
+
);
|
|
175
|
+
for (const o of s.operations)
|
|
176
|
+
search.run('operation', o.operationName, o.operationPath, String(repoId));
|
|
177
|
+
return id;
|
|
178
|
+
}
|
|
179
|
+
export function insertHandler(
|
|
180
|
+
db: Db,
|
|
181
|
+
repoId: number,
|
|
182
|
+
h: HandlerClassFact,
|
|
183
|
+
): number {
|
|
184
|
+
const sid = Number(
|
|
185
|
+
db
|
|
186
|
+
.prepare(
|
|
187
|
+
'INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line) VALUES(?,?,?,?,?,?,?) RETURNING id',
|
|
188
|
+
)
|
|
189
|
+
.get(
|
|
190
|
+
repoId,
|
|
191
|
+
'class',
|
|
192
|
+
h.className,
|
|
193
|
+
h.className,
|
|
194
|
+
1,
|
|
195
|
+
h.sourceLine,
|
|
196
|
+
h.sourceLine,
|
|
197
|
+
)?.id,
|
|
198
|
+
);
|
|
199
|
+
const hid = Number(
|
|
200
|
+
db
|
|
201
|
+
.prepare(
|
|
202
|
+
'INSERT INTO handler_classes(repo_id,symbol_id,class_name,source_file,source_line) VALUES(?,?,?,?,?) RETURNING id',
|
|
203
|
+
)
|
|
204
|
+
.get(repoId, sid, h.className, h.sourceFile, h.sourceLine)?.id,
|
|
205
|
+
);
|
|
206
|
+
const stmt = db.prepare(
|
|
207
|
+
'INSERT INTO handler_methods(handler_class_id,method_name,decorator_kind,decorator_value,decorator_raw_expression,source_file,source_line) VALUES(?,?,?,?,?,?,?)',
|
|
208
|
+
);
|
|
209
|
+
for (const m of h.methods)
|
|
210
|
+
stmt.run(
|
|
211
|
+
hid,
|
|
212
|
+
m.methodName,
|
|
213
|
+
m.decoratorKind,
|
|
214
|
+
m.decoratorValue,
|
|
215
|
+
m.decoratorRawExpression,
|
|
216
|
+
m.sourceFile,
|
|
217
|
+
m.sourceLine,
|
|
218
|
+
);
|
|
219
|
+
return hid;
|
|
220
|
+
}
|
|
221
|
+
export function insertRegistrations(
|
|
222
|
+
db: Db,
|
|
223
|
+
repoId: number,
|
|
224
|
+
rows: HandlerRegistrationFact[],
|
|
225
|
+
): void {
|
|
226
|
+
const stmt = db.prepare(
|
|
227
|
+
'INSERT INTO handler_registrations(repo_id,handler_class_id,class_name,import_source,registration_file,registration_line,registration_kind,confidence) VALUES(?,?,?,?,?,?,?,?)',
|
|
228
|
+
);
|
|
229
|
+
for (const r of rows) {
|
|
230
|
+
const handlerClass = r.className
|
|
231
|
+
? (db
|
|
232
|
+
.prepare(
|
|
233
|
+
'SELECT id FROM handler_classes WHERE repo_id=? AND class_name=? ORDER BY id',
|
|
234
|
+
)
|
|
235
|
+
.all(repoId, r.className) as Array<{ id: number }>)
|
|
236
|
+
: [];
|
|
237
|
+
stmt.run(
|
|
238
|
+
repoId,
|
|
239
|
+
handlerClass.length === 1 ? handlerClass[0]?.id : null,
|
|
240
|
+
r.className,
|
|
241
|
+
r.importSource,
|
|
242
|
+
r.registrationFile,
|
|
243
|
+
r.registrationLine,
|
|
244
|
+
r.registrationKind,
|
|
245
|
+
r.confidence,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
export function insertBindings(
|
|
250
|
+
db: Db,
|
|
251
|
+
repoId: number,
|
|
252
|
+
rows: ServiceBindingFact[],
|
|
253
|
+
): void {
|
|
254
|
+
const stmt = db.prepare(
|
|
255
|
+
'INSERT INTO service_bindings(repo_id,variable_name,alias,alias_expr,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line,helper_chain_json) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
|
|
256
|
+
);
|
|
257
|
+
for (const r of rows)
|
|
258
|
+
stmt.run(
|
|
259
|
+
repoId,
|
|
260
|
+
r.variableName,
|
|
261
|
+
r.alias,
|
|
262
|
+
r.aliasExpr,
|
|
263
|
+
r.destinationExpr,
|
|
264
|
+
r.servicePathExpr,
|
|
265
|
+
r.isDynamic ? 1 : 0,
|
|
266
|
+
JSON.stringify(r.placeholders),
|
|
267
|
+
r.sourceFile,
|
|
268
|
+
r.sourceLine,
|
|
269
|
+
r.helperChain ? JSON.stringify(r.helperChain) : null,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
export function insertExecutableSymbols(db: Db, repoId: number, rows: ExecutableSymbolFact[]): void {
|
|
273
|
+
const stmt = db.prepare('INSERT INTO symbols(repo_id,file_id,kind,name,qualified_name,exported,start_line,end_line,start_offset,end_offset,source_file,exported_name,evidence_json) VALUES(?,(SELECT id FROM files WHERE repo_id=? AND relative_path=?),?,?,?,?,?,?,?,?,?,?,?)');
|
|
274
|
+
for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.kind, r.localName, r.qualifiedName, r.exported ? 1 : 0, r.startLine, r.endLine, r.startOffset, r.endOffset, r.sourceFile, r.exportedName, r.importExportEvidence ? JSON.stringify(r.importExportEvidence) : null);
|
|
275
|
+
}
|
|
276
|
+
export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]): void {
|
|
277
|
+
const callerStmt = db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1');
|
|
278
|
+
const insertStmt = db.prepare('INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)');
|
|
279
|
+
for (const r of rows) {
|
|
280
|
+
const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName) as { id?: number } | undefined;
|
|
281
|
+
const target = resolveSymbolCallTarget(db, repoId, r);
|
|
282
|
+
insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount }), target.reason);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function isRelativeImportedSymbolCall(r: SymbolCallFact): boolean {
|
|
286
|
+
return Boolean(r.importSource?.startsWith('.'));
|
|
287
|
+
}
|
|
288
|
+
function resolveSymbolCallTarget(db: Db, repoId: number, r: SymbolCallFact): { id: number | null; status: string; reason: string | null; strategy: string; candidateCount: number } {
|
|
289
|
+
const evidence = r.evidence as { relation?: unknown };
|
|
290
|
+
const localRows = db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND (name=? OR qualified_name=?) ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName) as Array<{ id: number }>;
|
|
291
|
+
if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: 'resolved', reason: null, strategy: 'same_file_exact', candidateCount: 1 };
|
|
292
|
+
if (localRows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple same-file symbol targets matched exactly', strategy: 'same_file_exact', candidateCount: localRows.length };
|
|
293
|
+
if (evidence.relation === 'class_instance_method' && isRelativeImportedSymbolCall(r)) {
|
|
294
|
+
const classRows = db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName) as Array<{ id: number }>;
|
|
295
|
+
if (classRows.length === 1) return { id: classRows[0]?.id ?? null, status: 'resolved', reason: null, strategy: 'relative_import_class_instance_method', candidateCount: 1 };
|
|
296
|
+
if (classRows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple relative class instance method targets matched exactly', strategy: 'relative_import_class_instance_method', candidateCount: classRows.length };
|
|
297
|
+
}
|
|
298
|
+
const rows = db.prepare('SELECT id,kind,evidence_json evidenceJson FROM symbols WHERE repo_id=? AND source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?) ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName) as Array<{ id: number; kind?: string; evidenceJson?: string | null }>;
|
|
299
|
+
if (evidence.relation === 'relative_import_proxy_member' && rows.length > 1) {
|
|
300
|
+
const objectMapRows = rows.filter((row) => String(row.evidenceJson ?? '').includes('exported_object_shorthand') || String(row.evidenceJson ?? '').includes('exported_object_literal'));
|
|
301
|
+
if (objectMapRows.length > 0) {
|
|
302
|
+
const concrete = rows.find((row) => row.kind !== 'object_alias') ?? objectMapRows[0];
|
|
303
|
+
return { id: concrete?.id ?? null, status: 'resolved', reason: null, strategy: 'proxy_member_exported_object_map', candidateCount: rows.length };
|
|
304
|
+
}
|
|
305
|
+
return { id: null, status: 'ambiguous', reason: 'Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous', strategy: 'proxy_member_no_global_name_fallback', candidateCount: rows.length };
|
|
306
|
+
}
|
|
307
|
+
if (rows.length === 1) return { id: rows[0]?.id ?? null, status: 'resolved', reason: null, strategy: evidence.relation === 'relative_import_proxy_member' ? 'proxy_member_unique_exported_candidate' : 'relative_import_exported_exact', candidateCount: 1 };
|
|
308
|
+
if (rows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple exported symbol targets matched exactly', strategy: 'exported_exact', candidateCount: rows.length };
|
|
309
|
+
return { id: null, status: 'unresolved', reason: 'No local symbol target matched exactly', strategy: evidence.relation === 'relative_import_proxy_member' ? 'proxy_member_no_global_name_fallback' : 'exact_symbol_match', candidateCount: 0 };
|
|
310
|
+
}
|
|
311
|
+
export function insertCalls(
|
|
312
|
+
db: Db,
|
|
313
|
+
repoId: number,
|
|
314
|
+
rows: OutboundCallFact[],
|
|
315
|
+
): void {
|
|
316
|
+
const stmt = db.prepare(
|
|
317
|
+
'INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,evidence_json,external_target_kind,external_target_id,external_target_label,external_target_dynamic,service_binding_id) VALUES(?,COALESCE((SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? AND source_file=? ORDER BY CASE WHEN source_line<=? THEN 0 ELSE 1 END, ABS(source_line-?) ASC, id DESC LIMIT 1))',
|
|
318
|
+
);
|
|
319
|
+
for (const r of rows)
|
|
320
|
+
stmt.run(
|
|
321
|
+
repoId,
|
|
322
|
+
repoId,
|
|
323
|
+
r.sourceFile,
|
|
324
|
+
r.sourceSymbolQualifiedName,
|
|
325
|
+
repoId,
|
|
326
|
+
r.sourceFile,
|
|
327
|
+
r.sourceLine,
|
|
328
|
+
r.sourceLine,
|
|
329
|
+
r.callType,
|
|
330
|
+
r.method,
|
|
331
|
+
r.operationPathExpr,
|
|
332
|
+
r.queryEntity,
|
|
333
|
+
r.eventNameExpr,
|
|
334
|
+
r.payloadSummary,
|
|
335
|
+
r.sourceFile,
|
|
336
|
+
r.sourceLine,
|
|
337
|
+
r.confidence,
|
|
338
|
+
r.unresolvedReason,
|
|
339
|
+
r.localServiceName,
|
|
340
|
+
r.localServiceLookup,
|
|
341
|
+
r.aliasChain ? JSON.stringify(r.aliasChain) : null,
|
|
342
|
+
r.evidence ? JSON.stringify(r.evidence) : null,
|
|
343
|
+
r.externalTarget?.kind ?? null,
|
|
344
|
+
r.externalTarget?.stableId ?? null,
|
|
345
|
+
r.externalTarget?.label ?? null,
|
|
346
|
+
r.externalTarget?.dynamic ? 1 : 0,
|
|
347
|
+
repoId,
|
|
348
|
+
r.serviceVariableName,
|
|
349
|
+
r.sourceFile,
|
|
350
|
+
r.sourceLine,
|
|
351
|
+
r.sourceLine,
|
|
352
|
+
);
|
|
353
|
+
}
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const schemaSql = `
|
|
2
|
+
CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
3
|
+
CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
4
|
+
CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
5
|
+
CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
6
|
+
CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
7
|
+
CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(service_id) REFERENCES cds_services(id) ON DELETE CASCADE);
|
|
8
|
+
CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, start_offset INTEGER, end_offset INTEGER, source_file TEXT, exported_name TEXT, evidence_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
|
|
9
|
+
CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
10
|
+
CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
13
|
+
CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, external_target_kind TEXT, external_target_id TEXT, external_target_label TEXT, external_target_dynamic INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
15
|
+
CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
16
|
+
CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE SET NULL);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
|
|
23
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
|
|
24
|
+
`;
|