@ophan/core 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -1,107 +1,70 @@
1
1
  # @ophan/core
2
2
 
3
- Multi-language analysis engine combining language-native AST parsing with Claude-powered security analysis and documentation.
4
-
5
- ## Capabilities
6
-
7
- ### Static Analysis
8
- - Function extraction via pluggable language parsers (`src/parsers/`)
9
- - TypeScript/JavaScript: TypeScript compiler API
10
- - Python: Python's own `ast` module via subprocess (zero npm deps)
11
- - Content hashing (SHA256) for change detection and content-addressed storage
12
- - AST traversal for control flow understanding
13
-
14
- ### Content-Addressed Storage
15
- Analysis is keyed by SHA256 of function source code:
16
- - Same function on any branch = same hash = same analysis entry
17
- - Skip analysis entirely when hash already exists in DB
18
- - Insert-only sync — no conflict resolution needed
19
- - Orphaned hashes accumulate harmlessly until manual `ophan gc` (30-day grace period, safe for branch switching)
20
-
21
- ### Two-Table Schema
22
- - `function_analysis` — `content_hash → analysis JSON, model_version, created_at, language, entity_type` (synced to cloud)
23
- - `file_functions` — `file_path + function_name → content_hash, file_mtime, language, entity_type` (local only). No line numbers — resolved at runtime via LSP/parser.
24
-
25
- ### Incremental Scanning
26
- 1. Check `file_mtime` against stored value — skip unchanged files entirely (no parse)
27
- 2. Re-parse changed files, extract functions, compute hashes
28
- 3. Look up hash in `function_analysis` — skip if exists
29
- 4. Only call Claude API for functions with new/unknown hashes
30
- 5. Update `file_functions` with file path, function name, content_hash, mtime
31
-
32
- ### AI Analysis
33
- Leverages Claude to provide:
34
- - Natural language function descriptions
35
- - Parameter and return type documentation
36
- - Security vulnerability identification
37
- - Data flow classification
3
+ The analysis engine behind [Ophan](https://ophan.dev) AI-powered security analysis and documentation for codebases.
4
+
5
+ This package provides the core analysis pipeline: source code parsing, function extraction, Claude-powered security analysis, and local SQLite storage. Used by [`@ophan/cli`](https://www.npmjs.com/package/@ophan/cli) and the [Ophan VS Code extension](https://marketplace.visualstudio.com/items?itemName=ophan.ophan).
6
+
7
+ ## What It Does
8
+
9
+ - **Parses source code** using language-native ASTs (TypeScript compiler API, Python's `ast` module)
10
+ - **Extracts functions** and computes SHA256 content hashes for change detection
11
+ - **Analyzes with Claude** to detect security vulnerabilities and generate documentation
12
+ - **Stores results** in a local SQLite database, keyed by content hash
38
13
 
39
14
  ### Security Detection
40
- Identifies common vulnerabilities:
41
- - SQL injection risks
42
- - Cross-site scripting (XSS) potential
43
- - Hardcoded secrets and credentials
44
- - Unsanitized user input
45
- - Path traversal vulnerabilities
46
-
47
- ### Data Flow Tagging
48
- Classifies functions by data they handle:
49
- - `user_input` — Processes user-provided data
50
- - `pii` — Handles personally identifiable information
51
- - `credentials` — Works with authentication data
52
- - `database` — Database read/write operations
53
- - `external_api` — Makes external HTTP requests
54
- - `file_system` — File I/O operations
55
- - `config` — Configuration management
56
- - `internal` — Internal system operations
57
-
58
- ## Storage
59
-
60
- SQLite database at `.ophan/index.db` (gitignored, per-repo). Designed for:
61
- - Fast querying by content hash, file path, and line number
62
- - Incremental updates (only new hashes trigger analysis)
63
- - JSON storage for structured analysis results
64
- - Branch-agnostic analysis with branch-specific location mappings
65
-
66
- ## Multi-Language Architecture
67
-
68
- ### Parser Interface (`src/parsers/`)
69
-
70
- Function extraction is handled by pluggable, language-specific parsers. Each implements the `LanguageParser` interface:
71
15
 
72
- ```typescript
73
- interface LanguageParser {
74
- language: string; // e.g. "typescript", "python"
75
- extensions: string[]; // e.g. [".ts", ".tsx"]
76
- extractFunctions(filePath: string): FunctionInfo[];
77
- }
78
- ```
16
+ Identifies vulnerabilities including SQL injection, XSS, hardcoded secrets, path traversal, insecure deserialization, and unsanitized user input.
17
+
18
+ ### Data Flow Classification
19
+
20
+ Tags functions by the data they handle: user input, PII, credentials, database operations, external APIs, file system access, and more.
21
+
22
+ ### Auto-Documentation
23
+
24
+ Generates plain-English descriptions, parameter documentation, and return type documentation for every function.
79
25
 
80
- **Current parsers:**
81
- - **TypeScript/JavaScript** (`parsers/typescript.ts`): Uses the TypeScript compiler API. Handles `.ts`, `.tsx`, `.js`, `.jsx`.
82
- - **Python** (`parsers/python.ts`): Shells out to `python3` with an inline `ast` module script. Handles `.py`. Gracefully skips if `python3` not installed.
26
+ ## Supported Languages
83
27
 
84
- **To add a new language** (e.g. Go, Java):
85
- 1. Create `parsers/go.ts` implementing `LanguageParser`
86
- 2. Register it in `parsers/index.ts`
87
- 3. That's it the glob pattern, analysis pipeline, DB storage, and sync all pick it up automatically
28
+ | Language | Parser | Extensions |
29
+ |----------|--------|------------|
30
+ | TypeScript | TypeScript compiler API | `.ts`, `.tsx` |
31
+ | JavaScript | TypeScript compiler API | `.js`, `.jsx` |
32
+ | Python | Python `ast` module | `.py` |
88
33
 
89
- **Why no tree-sitter?** Each language uses its own native tooling (TS compiler, Python's ast, go/parser, etc.) rather than a universal parser framework. This avoids native C dependencies, grammar management, and adds zero npm packages per language.
34
+ ## Usage
90
35
 
91
- ### Schema Fields
36
+ Most users should use [`@ophan/cli`](https://www.npmjs.com/package/@ophan/cli) or the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ophan.ophan) instead of importing this package directly.
92
37
 
93
- Both tables carry `language` and `entity_type` fields set at parse time:
94
- - **`language`**: Derived from file extension (`.ts` → `"typescript"`, `.py` → `"python"`). Drives prompt template selection and language-specific security flags.
95
- - **`entity_type`**: Derived from AST node kind (`isMethodDeclaration` → `"method"`, `FunctionDef` inside `ClassDef` → `"method"`). Enables class-level vs function-level analysis.
38
+ `@ophan/core` is published for tools that need to build on the Ophan analysis pipeline programmatically.
39
+
40
+ ```typescript
41
+ import { analyzeRepository, initDb } from '@ophan/core';
42
+
43
+ const db = initDb('/path/to/repo');
44
+ await analyzeRepository(db, '/path/to/repo', {
45
+ apiKey: process.env.ANTHROPIC_API_KEY,
46
+ });
47
+ ```
48
+
49
+ ### Schemas
50
+
51
+ Zod schemas and TypeScript types for Ophan's analysis data are available as a lightweight subpath import — no native dependencies required:
52
+
53
+ ```typescript
54
+ import {
55
+ ClaudeAnalysisResponse,
56
+ SECURITY_FLAG_LABELS,
57
+ DATA_TAG_LABELS,
58
+ } from '@ophan/core/schemas';
59
+ ```
96
60
 
97
- ### Migration Safety
61
+ ## Resources
98
62
 
99
- Both tables use `DEFAULT 'typescript'` and `DEFAULT 'function'` for the new columns. Existing databases are migrated via `ALTER TABLE ADD COLUMN` on init — safe, no data loss.
63
+ - [Documentation](https://docs.ophan.dev)
64
+ - [CLI Package](https://www.npmjs.com/package/@ophan/cli)
65
+ - [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ophan.ophan)
66
+ - [GitHub](https://github.com/nicholasgriffintn/ophan)
100
67
 
101
- ## Extensibility
68
+ ## License
102
69
 
103
- Designed to support:
104
- - Additional programming languages via parser interface (Go, Java, Rust planned)
105
- - Alternative AI models (Bedrock for enterprise)
106
- - Custom security rules
107
- - Supabase sync for cloud features
70
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ophan/core",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "main": "./src/index.ts",
5
5
  "types": "./src/index.ts",
6
6
  "exports": {
@@ -8,7 +8,9 @@
8
8
  "./schemas": "./src/schemas.ts",
9
9
  "./test-utils": "./src/test-utils.ts"
10
10
  },
11
- "files": ["dist"],
11
+ "files": [
12
+ "dist"
13
+ ],
12
14
  "publishConfig": {
13
15
  "main": "./dist/index.js",
14
16
  "types": "./dist/index.d.ts",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -1,492 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- const vitest_1 = require("vitest");
40
- const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
41
- const fs = __importStar(require("fs"));
42
- const os = __importStar(require("os"));
43
- const path = __importStar(require("path"));
44
- const index_1 = require("./index");
45
- const test_utils_1 = require("./test-utils");
46
- // ============ mergeAnalysisRows ============
47
- (0, vitest_1.describe)("mergeAnalysisRows", () => {
48
- (0, vitest_1.it)("merges documentation + security rows into unified shape", () => {
49
- const result = (0, index_1.mergeAnalysisRows)([
50
- {
51
- analysis_type: "documentation",
52
- analysis: JSON.stringify({
53
- description: "Fetches user",
54
- params: [{ name: "id", type: "string", description: "user id" }],
55
- returns: { type: "User", description: "the user" },
56
- }),
57
- },
58
- {
59
- analysis_type: "security",
60
- analysis: JSON.stringify({
61
- dataTags: ["database", "pii"],
62
- securityFlags: ["sql_injection"],
63
- }),
64
- },
65
- ]);
66
- (0, vitest_1.expect)(result.description).toBe("Fetches user");
67
- (0, vitest_1.expect)(result.params).toEqual([
68
- { name: "id", type: "string", description: "user id" },
69
- ]);
70
- (0, vitest_1.expect)(result.returns).toEqual({ type: "User", description: "the user" });
71
- (0, vitest_1.expect)(result.dataTags).toEqual(["database", "pii"]);
72
- (0, vitest_1.expect)(result.securityFlags).toEqual(["sql_injection"]);
73
- });
74
- (0, vitest_1.it)("handles documentation-only (missing security row)", () => {
75
- const result = (0, index_1.mergeAnalysisRows)([
76
- {
77
- analysis_type: "documentation",
78
- analysis: JSON.stringify({
79
- description: "Test",
80
- params: [],
81
- returns: { type: "void", description: "" },
82
- }),
83
- },
84
- ]);
85
- (0, vitest_1.expect)(result.description).toBe("Test");
86
- (0, vitest_1.expect)(result.dataTags).toEqual([]);
87
- (0, vitest_1.expect)(result.securityFlags).toEqual([]);
88
- });
89
- (0, vitest_1.it)("handles security-only (missing documentation row)", () => {
90
- const result = (0, index_1.mergeAnalysisRows)([
91
- {
92
- analysis_type: "security",
93
- analysis: JSON.stringify({
94
- dataTags: ["internal"],
95
- securityFlags: [],
96
- }),
97
- },
98
- ]);
99
- (0, vitest_1.expect)(result.description).toBe("");
100
- (0, vitest_1.expect)(result.params).toEqual([]);
101
- (0, vitest_1.expect)(result.returns).toEqual({ type: "unknown", description: "" });
102
- (0, vitest_1.expect)(result.dataTags).toEqual(["internal"]);
103
- });
104
- (0, vitest_1.it)("handles empty rows array", () => {
105
- const result = (0, index_1.mergeAnalysisRows)([]);
106
- (0, vitest_1.expect)(result.description).toBe("");
107
- (0, vitest_1.expect)(result.params).toEqual([]);
108
- (0, vitest_1.expect)(result.returns).toEqual({ type: "unknown", description: "" });
109
- (0, vitest_1.expect)(result.dataTags).toEqual([]);
110
- (0, vitest_1.expect)(result.securityFlags).toEqual([]);
111
- });
112
- (0, vitest_1.it)("ignores unknown analysis_type", () => {
113
- const result = (0, index_1.mergeAnalysisRows)([
114
- {
115
- analysis_type: "future_type",
116
- analysis: JSON.stringify({ foo: "bar" }),
117
- },
118
- ]);
119
- (0, vitest_1.expect)(result.description).toBe("");
120
- (0, vitest_1.expect)(result.securityFlags).toEqual([]);
121
- });
122
- (0, vitest_1.it)("last row wins when duplicate analysis_type", () => {
123
- const result = (0, index_1.mergeAnalysisRows)([
124
- {
125
- analysis_type: "documentation",
126
- analysis: JSON.stringify({
127
- description: "First",
128
- params: [],
129
- returns: { type: "void", description: "" },
130
- }),
131
- },
132
- {
133
- analysis_type: "documentation",
134
- analysis: JSON.stringify({
135
- description: "Second",
136
- params: [],
137
- returns: { type: "string", description: "overwritten" },
138
- }),
139
- },
140
- ]);
141
- (0, vitest_1.expect)(result.description).toBe("Second");
142
- (0, vitest_1.expect)(result.returns.type).toBe("string");
143
- });
144
- });
145
- // ============ migrateToAnalysisTypes ============
146
- (0, vitest_1.describe)("migrateToAnalysisTypes", () => {
147
- (0, vitest_1.it)("splits single legacy row into documentation + security rows", () => {
148
- const db = (0, test_utils_1.createLegacyTestDb)();
149
- db.prepare(`INSERT INTO function_analysis
150
- (content_hash, analysis, model_version, created_at, last_seen_at, language, entity_type, synced_at)
151
- VALUES (?, ?, 'claude-3', 1000, 2000, 'typescript', 'function', 5000)`).run("abc123", JSON.stringify({
152
- description: "Test fn",
153
- params: [{ name: "x", type: "number", description: "input" }],
154
- returns: { type: "number", description: "output" },
155
- dataTags: ["internal"],
156
- securityFlags: ["xss"],
157
- }));
158
- (0, index_1.migrateToAnalysisTypes)(db);
159
- const rows = db
160
- .prepare("SELECT content_hash, analysis_type, analysis, synced_at FROM function_analysis ORDER BY analysis_type")
161
- .all();
162
- (0, vitest_1.expect)(rows).toHaveLength(2);
163
- const docRow = rows.find((r) => r.analysis_type === "documentation");
164
- const secRow = rows.find((r) => r.analysis_type === "security");
165
- (0, vitest_1.expect)(docRow).toBeDefined();
166
- (0, vitest_1.expect)(secRow).toBeDefined();
167
- const doc = JSON.parse(docRow.analysis);
168
- (0, vitest_1.expect)(doc.description).toBe("Test fn");
169
- (0, vitest_1.expect)(doc.params).toEqual([
170
- { name: "x", type: "number", description: "input" },
171
- ]);
172
- const sec = JSON.parse(secRow.analysis);
173
- (0, vitest_1.expect)(sec.dataTags).toEqual(["internal"]);
174
- (0, vitest_1.expect)(sec.securityFlags).toEqual(["xss"]);
175
- // synced_at should be NULL (forced re-sync)
176
- (0, vitest_1.expect)(docRow.synced_at).toBeNull();
177
- (0, vitest_1.expect)(secRow.synced_at).toBeNull();
178
- db.close();
179
- });
180
- (0, vitest_1.it)("handles NULL language/entity_type with COALESCE defaults", () => {
181
- const db = (0, test_utils_1.createLegacyTestDb)();
182
- db.prepare(`INSERT INTO function_analysis
183
- (content_hash, analysis, model_version, created_at, last_seen_at, language, entity_type)
184
- VALUES (?, ?, 'claude-3', 1000, 2000, NULL, NULL)`).run("hash1", JSON.stringify({ description: "Test" }));
185
- (0, index_1.migrateToAnalysisTypes)(db);
186
- const rows = db
187
- .prepare("SELECT language, entity_type FROM function_analysis WHERE content_hash = 'hash1'")
188
- .all();
189
- for (const row of rows) {
190
- (0, vitest_1.expect)(row.language).toBe("typescript");
191
- (0, vitest_1.expect)(row.entity_type).toBe("function");
192
- }
193
- db.close();
194
- });
195
- (0, vitest_1.it)("splits multiple legacy rows", () => {
196
- const db = (0, test_utils_1.createLegacyTestDb)();
197
- for (let i = 1; i <= 3; i++) {
198
- db.prepare(`INSERT INTO function_analysis
199
- (content_hash, analysis, model_version, created_at, last_seen_at)
200
- VALUES (?, ?, 'claude-3', 1000, 2000)`).run(`hash${i}`, JSON.stringify({ description: `Fn ${i}` }));
201
- }
202
- (0, index_1.migrateToAnalysisTypes)(db);
203
- const count = db
204
- .prepare("SELECT COUNT(*) as c FROM function_analysis")
205
- .get();
206
- (0, vitest_1.expect)(count.c).toBe(6); // 3 hashes x 2 types
207
- db.close();
208
- });
209
- (0, vitest_1.it)("new table has correct composite PK (content_hash, analysis_type)", () => {
210
- const db = (0, test_utils_1.createLegacyTestDb)();
211
- db.prepare(`INSERT INTO function_analysis
212
- (content_hash, analysis, model_version, created_at, last_seen_at)
213
- VALUES (?, ?, 'claude-3', 1000, 2000)`).run("hash1", JSON.stringify({}));
214
- (0, index_1.migrateToAnalysisTypes)(db);
215
- // Inserting duplicate (same hash + type) should fail
216
- (0, vitest_1.expect)(() => db
217
- .prepare(`INSERT INTO function_analysis
218
- (content_hash, analysis_type, analysis, model_version, schema_version, created_at, last_seen_at, language, entity_type)
219
- VALUES ('hash1', 'documentation', '{}', 'test', 1, 1000, 2000, 'typescript', 'function')`)
220
- .run()).toThrow();
221
- db.close();
222
- });
223
- });
224
- // ============ gcAnalysis ============
225
- (0, vitest_1.describe)("gcAnalysis", () => {
226
- let tmpDir;
227
- let dbPath;
228
- function setupDb(setup) {
229
- const db = new better_sqlite3_1.default(dbPath);
230
- // Create schema
231
- const memDb = (0, test_utils_1.createTestDb)();
232
- const tables = memDb
233
- .prepare("SELECT sql FROM sqlite_master WHERE type='table'")
234
- .all();
235
- for (const t of tables) {
236
- db.exec(t.sql + ";");
237
- }
238
- const indexes = memDb
239
- .prepare("SELECT sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL")
240
- .all();
241
- for (const idx of indexes) {
242
- db.exec(idx.sql + ";");
243
- }
244
- memDb.close();
245
- setup(db);
246
- db.close();
247
- }
248
- (0, vitest_1.beforeEach)(() => {
249
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ophan-gc-test-"));
250
- dbPath = path.join(tmpDir, "index.db");
251
- });
252
- (0, vitest_1.afterEach)(() => {
253
- fs.rmSync(tmpDir, { recursive: true, force: true });
254
- });
255
- (0, vitest_1.it)("deletes orphaned analysis older than grace period", () => {
256
- const ninetyDaysAgo = Math.floor(Date.now() / 1000) - 90 * 24 * 60 * 60;
257
- setupDb((db) => {
258
- (0, test_utils_1.insertAnalysisPair)(db, "orphan1", { lastSeenAt: ninetyDaysAgo });
259
- // No file_functions entry → orphaned
260
- });
261
- const result = (0, index_1.gcAnalysis)(dbPath, 30);
262
- (0, vitest_1.expect)(result.deleted).toBe(2); // doc + sec rows
263
- const db = new better_sqlite3_1.default(dbPath, { readonly: true });
264
- const remaining = db
265
- .prepare("SELECT COUNT(*) as c FROM function_analysis")
266
- .get();
267
- (0, vitest_1.expect)(remaining.c).toBe(0);
268
- db.close();
269
- });
270
- (0, vitest_1.it)("preserves analysis still referenced by file_functions", () => {
271
- const ninetyDaysAgo = Math.floor(Date.now() / 1000) - 90 * 24 * 60 * 60;
272
- setupDb((db) => {
273
- (0, test_utils_1.insertAnalysisPair)(db, "active1", { lastSeenAt: ninetyDaysAgo });
274
- (0, test_utils_1.insertFileFunction)(db, "/src/foo.ts", "foo", "active1");
275
- });
276
- const result = (0, index_1.gcAnalysis)(dbPath, 30);
277
- (0, vitest_1.expect)(result.deleted).toBe(0);
278
- });
279
- (0, vitest_1.it)("preserves orphaned analysis within grace period", () => {
280
- const fiveDaysAgo = Math.floor(Date.now() / 1000) - 5 * 24 * 60 * 60;
281
- setupDb((db) => {
282
- (0, test_utils_1.insertAnalysisPair)(db, "recent_orphan", { lastSeenAt: fiveDaysAgo });
283
- });
284
- const result = (0, index_1.gcAnalysis)(dbPath, 30);
285
- (0, vitest_1.expect)(result.deleted).toBe(0);
286
- });
287
- (0, vitest_1.it)("creates tombstones in function_gc before deleting", () => {
288
- const ninetyDaysAgo = Math.floor(Date.now() / 1000) - 90 * 24 * 60 * 60;
289
- setupDb((db) => {
290
- (0, test_utils_1.insertAnalysisPair)(db, "doomed", { lastSeenAt: ninetyDaysAgo });
291
- });
292
- (0, index_1.gcAnalysis)(dbPath, 30);
293
- const db = new better_sqlite3_1.default(dbPath, { readonly: true });
294
- const tombstones = db
295
- .prepare("SELECT content_hash, analysis_type FROM function_gc ORDER BY analysis_type")
296
- .all();
297
- (0, vitest_1.expect)(tombstones).toHaveLength(2);
298
- (0, vitest_1.expect)(tombstones.map((t) => t.analysis_type).sort()).toEqual([
299
- "documentation",
300
- "security",
301
- ]);
302
- db.close();
303
- });
304
- (0, vitest_1.it)("handles empty database", () => {
305
- setupDb(() => { });
306
- const result = (0, index_1.gcAnalysis)(dbPath, 30);
307
- (0, vitest_1.expect)(result.deleted).toBe(0);
308
- });
309
- });
310
- // ============ importAnalysis ============
311
- (0, vitest_1.describe)("importAnalysis", () => {
312
- let tmpDir;
313
- let dbPath;
314
- function setupDb(setup) {
315
- const db = new better_sqlite3_1.default(dbPath);
316
- const memDb = (0, test_utils_1.createTestDb)();
317
- const tables = memDb
318
- .prepare("SELECT sql FROM sqlite_master WHERE type='table'")
319
- .all();
320
- for (const t of tables) {
321
- db.exec(t.sql + ";");
322
- }
323
- memDb.close();
324
- if (setup)
325
- setup(db);
326
- db.close();
327
- }
328
- (0, vitest_1.beforeEach)(() => {
329
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ophan-import-test-"));
330
- dbPath = path.join(tmpDir, "index.db");
331
- });
332
- (0, vitest_1.afterEach)(() => {
333
- fs.rmSync(tmpDir, { recursive: true, force: true });
334
- });
335
- (0, vitest_1.it)("imports new analysis rows", () => {
336
- setupDb();
337
- const count = (0, index_1.importAnalysis)(dbPath, [
338
- {
339
- content_hash: "abc",
340
- analysis_type: "documentation",
341
- analysis: (0, test_utils_1.makeDocAnalysisJson)(),
342
- model_version: "test",
343
- schema_version: 1,
344
- language: "typescript",
345
- entity_type: "function",
346
- },
347
- ]);
348
- (0, vitest_1.expect)(count).toBe(1);
349
- const db = new better_sqlite3_1.default(dbPath, { readonly: true });
350
- const row = db
351
- .prepare("SELECT * FROM function_analysis WHERE content_hash = 'abc'")
352
- .get();
353
- (0, vitest_1.expect)(row).toBeDefined();
354
- (0, vitest_1.expect)(row.analysis_type).toBe("documentation");
355
- db.close();
356
- });
357
- (0, vitest_1.it)("skips existing rows (INSERT OR IGNORE)", () => {
358
- setupDb((db) => {
359
- (0, test_utils_1.insertAnalysisPair)(db, "exists");
360
- });
361
- const count = (0, index_1.importAnalysis)(dbPath, [
362
- {
363
- content_hash: "exists",
364
- analysis_type: "documentation",
365
- analysis: (0, test_utils_1.makeDocAnalysisJson)({ description: "SHOULD NOT OVERWRITE" }),
366
- model_version: "test",
367
- schema_version: 1,
368
- language: "typescript",
369
- entity_type: "function",
370
- },
371
- ]);
372
- (0, vitest_1.expect)(count).toBe(0);
373
- // Verify original data unchanged
374
- const db = new better_sqlite3_1.default(dbPath, { readonly: true });
375
- const row = db
376
- .prepare("SELECT analysis FROM function_analysis WHERE content_hash = 'exists' AND analysis_type = 'documentation'")
377
- .get();
378
- const parsed = JSON.parse(row.analysis);
379
- (0, vitest_1.expect)(parsed.description).toBe("Test function description");
380
- db.close();
381
- });
382
- (0, vitest_1.it)("sets synced_at on imported rows", () => {
383
- setupDb();
384
- (0, index_1.importAnalysis)(dbPath, [
385
- {
386
- content_hash: "new1",
387
- analysis_type: "security",
388
- analysis: (0, test_utils_1.makeSecAnalysisJson)(),
389
- model_version: "test",
390
- schema_version: 1,
391
- language: "typescript",
392
- entity_type: "function",
393
- },
394
- ]);
395
- const db = new better_sqlite3_1.default(dbPath, { readonly: true });
396
- const row = db
397
- .prepare("SELECT synced_at FROM function_analysis WHERE content_hash = 'new1'")
398
- .get();
399
- (0, vitest_1.expect)(row.synced_at).not.toBeNull();
400
- db.close();
401
- });
402
- (0, vitest_1.it)("returns 0 for empty rows array", () => {
403
- setupDb();
404
- (0, vitest_1.expect)((0, index_1.importAnalysis)(dbPath, [])).toBe(0);
405
- });
406
- (0, vitest_1.it)("handles mixed new and existing rows", () => {
407
- setupDb((db) => {
408
- (0, test_utils_1.insertAnalysisPair)(db, "exists");
409
- });
410
- const count = (0, index_1.importAnalysis)(dbPath, [
411
- {
412
- content_hash: "exists",
413
- analysis_type: "documentation",
414
- analysis: (0, test_utils_1.makeDocAnalysisJson)(),
415
- model_version: "test",
416
- schema_version: 1,
417
- language: "typescript",
418
- entity_type: "function",
419
- },
420
- {
421
- content_hash: "new1",
422
- analysis_type: "documentation",
423
- analysis: (0, test_utils_1.makeDocAnalysisJson)(),
424
- model_version: "test",
425
- schema_version: 1,
426
- language: "typescript",
427
- entity_type: "function",
428
- },
429
- {
430
- content_hash: "new2",
431
- analysis_type: "security",
432
- analysis: (0, test_utils_1.makeSecAnalysisJson)(),
433
- model_version: "test",
434
- schema_version: 1,
435
- language: "python",
436
- entity_type: "method",
437
- },
438
- ]);
439
- (0, vitest_1.expect)(count).toBe(2);
440
- });
441
- });
442
- // ============ findMissingHashes ============
443
- (0, vitest_1.describe)("findMissingHashes", () => {
444
- let tmpDir;
445
- let dbPath;
446
- function setupDb(setup) {
447
- const db = new better_sqlite3_1.default(dbPath);
448
- const memDb = (0, test_utils_1.createTestDb)();
449
- const tables = memDb
450
- .prepare("SELECT sql FROM sqlite_master WHERE type='table'")
451
- .all();
452
- for (const t of tables) {
453
- db.exec(t.sql + ";");
454
- }
455
- memDb.close();
456
- setup(db);
457
- db.close();
458
- }
459
- (0, vitest_1.beforeEach)(() => {
460
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ophan-missing-test-"));
461
- dbPath = path.join(tmpDir, "index.db");
462
- });
463
- (0, vitest_1.afterEach)(() => {
464
- fs.rmSync(tmpDir, { recursive: true, force: true });
465
- });
466
- (0, vitest_1.it)("returns hashes in file_functions but not in function_analysis", () => {
467
- setupDb((db) => {
468
- (0, test_utils_1.insertFileFunction)(db, "/src/foo.ts", "foo", "hash1");
469
- (0, test_utils_1.insertFileFunction)(db, "/src/bar.ts", "bar", "hash2");
470
- // Only hash1 has analysis
471
- (0, test_utils_1.insertAnalysisPair)(db, "hash1");
472
- });
473
- const missing = (0, index_1.findMissingHashes)(dbPath);
474
- (0, vitest_1.expect)(missing).toEqual(["hash2"]);
475
- });
476
- (0, vitest_1.it)("returns empty when all hashes are analyzed", () => {
477
- setupDb((db) => {
478
- (0, test_utils_1.insertFileFunction)(db, "/src/foo.ts", "foo", "hash1");
479
- (0, test_utils_1.insertAnalysisPair)(db, "hash1");
480
- });
481
- const missing = (0, index_1.findMissingHashes)(dbPath);
482
- (0, vitest_1.expect)(missing).toEqual([]);
483
- });
484
- (0, vitest_1.it)("deduplicates — same hash in multiple files returned once", () => {
485
- setupDb((db) => {
486
- (0, test_utils_1.insertFileFunction)(db, "/src/a.ts", "fn", "shared_hash");
487
- (0, test_utils_1.insertFileFunction)(db, "/src/b.ts", "fn", "shared_hash");
488
- });
489
- const missing = (0, index_1.findMissingHashes)(dbPath);
490
- (0, vitest_1.expect)(missing).toEqual(["shared_hash"]);
491
- });
492
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=python.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"python.test.d.ts","sourceRoot":"","sources":["../../src/parsers/python.test.ts"],"names":[],"mappings":""}
@@ -1,96 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- const vitest_1 = require("vitest");
37
- const child_process_1 = require("child_process");
38
- const path = __importStar(require("path"));
39
- const python_1 = require("./python");
40
- const parser = new python_1.PythonParser();
41
- const fixturesDir = path.join(__dirname, "__fixtures__");
42
- // Must be synchronous at module level — describe.skipIf evaluates immediately
43
- let pythonAvailable = false;
44
- try {
45
- (0, child_process_1.execFileSync)("python3", ["--version"], { stdio: "ignore" });
46
- pythonAvailable = true;
47
- }
48
- catch {
49
- pythonAvailable = false;
50
- }
51
- (0, vitest_1.describe)("PythonParser", () => {
52
- vitest_1.describe.skipIf(!pythonAvailable)("method detection", () => {
53
- (0, vitest_1.it)('class methods get entityType "method"', () => {
54
- const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.py"));
55
- const getUser = functions.find((f) => f.name === "get_user");
56
- const createUser = functions.find((f) => f.name === "create_user");
57
- (0, vitest_1.expect)(getUser?.entityType).toBe("method");
58
- (0, vitest_1.expect)(createUser?.entityType).toBe("method");
59
- });
60
- (0, vitest_1.it)('top-level functions get entityType "function"', () => {
61
- const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.py"));
62
- const standalone = functions.find((f) => f.name === "standalone");
63
- (0, vitest_1.expect)(standalone?.entityType).toBe("function");
64
- });
65
- });
66
- vitest_1.describe.skipIf(!pythonAvailable)("async function handling", () => {
67
- (0, vitest_1.it)("extracts async def functions", () => {
68
- const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
69
- const fetchData = functions.find((f) => f.name === "fetch_data");
70
- (0, vitest_1.expect)(fetchData).toBeDefined();
71
- (0, vitest_1.expect)(fetchData?.language).toBe("python");
72
- });
73
- });
74
- vitest_1.describe.skipIf(!pythonAvailable)("basic extraction", () => {
75
- (0, vitest_1.it)("extracts all functions from simple file", () => {
76
- const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
77
- const names = functions.map((f) => f.name);
78
- (0, vitest_1.expect)(names).toContain("greet");
79
- (0, vitest_1.expect)(names).toContain("fetch_data");
80
- (0, vitest_1.expect)(names).toContain("add");
81
- });
82
- (0, vitest_1.it)("all extracted functions have contentHash set", () => {
83
- const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
84
- for (const fn of functions) {
85
- (0, vitest_1.expect)(fn.contentHash).toBeDefined();
86
- (0, vitest_1.expect)(fn.contentHash.length).toBe(64);
87
- }
88
- });
89
- (0, vitest_1.it)("all functions have language python", () => {
90
- const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
91
- for (const fn of functions) {
92
- (0, vitest_1.expect)(fn.language).toBe("python");
93
- }
94
- });
95
- });
96
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=typescript.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"typescript.test.d.ts","sourceRoot":"","sources":["../../src/parsers/typescript.test.ts"],"names":[],"mappings":""}
@@ -1,106 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- const vitest_1 = require("vitest");
37
- const path = __importStar(require("path"));
38
- const typescript_1 = require("./typescript");
39
- const parser = new typescript_1.TypeScriptParser();
40
- const fixturesDir = path.join(__dirname, "__fixtures__");
41
- (0, vitest_1.describe)("TypeScriptParser", () => {
42
- (0, vitest_1.describe)("name extraction", () => {
43
- (0, vitest_1.it)("extracts name from const arrow function declarations", () => {
44
- const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
45
- const names = functions.map((f) => f.name);
46
- (0, vitest_1.expect)(names).toContain("greet");
47
- (0, vitest_1.expect)(names).toContain("add");
48
- (0, vitest_1.expect)(names).toContain("handler");
49
- });
50
- (0, vitest_1.it)("skips unnamed default export arrows", () => {
51
- const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
52
- const names = functions.map((f) => f.name);
53
- // The default export `(x: number) => x * 2` has no variable declaration parent
54
- // so it should not appear in the extracted functions
55
- const unnamed = functions.filter((f) => f.name === null || f.name === undefined);
56
- (0, vitest_1.expect)(unnamed).toHaveLength(0);
57
- });
58
- (0, vitest_1.it)("skips callback arrows without variable declaration parent", () => {
59
- const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
60
- const names = functions.map((f) => f.name);
61
- // The `(n) => n * 2` inside .map() should not appear
62
- // (its parent is a CallExpression argument, not a VariableDeclaration)
63
- (0, vitest_1.expect)(names).not.toContain("n");
64
- });
65
- });
66
- (0, vitest_1.describe)("entity type detection", () => {
67
- (0, vitest_1.it)('class methods get entityType "method"', () => {
68
- const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
69
- const getUser = functions.find((f) => f.name === "getUser");
70
- const createUser = functions.find((f) => f.name === "createUser");
71
- (0, vitest_1.expect)(getUser?.entityType).toBe("method");
72
- (0, vitest_1.expect)(createUser?.entityType).toBe("method");
73
- });
74
- (0, vitest_1.it)('standalone functions get entityType "function"', () => {
75
- const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
76
- const standalone = functions.find((f) => f.name === "standalone");
77
- (0, vitest_1.expect)(standalone?.entityType).toBe("function");
78
- });
79
- });
80
- (0, vitest_1.describe)("language detection", () => {
81
- (0, vitest_1.it)(".ts files get language typescript", () => {
82
- const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
83
- for (const fn of functions) {
84
- (0, vitest_1.expect)(fn.language).toBe("typescript");
85
- }
86
- });
87
- });
88
- (0, vitest_1.describe)("edge cases", () => {
89
- (0, vitest_1.it)("file with no functions returns empty array", () => {
90
- const functions = parser.extractFunctions(path.join(fixturesDir, "no-functions.ts"));
91
- (0, vitest_1.expect)(functions).toEqual([]);
92
- });
93
- (0, vitest_1.it)("all extracted functions have contentHash set", () => {
94
- const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
95
- for (const fn of functions) {
96
- (0, vitest_1.expect)(fn.contentHash).toBeDefined();
97
- (0, vitest_1.expect)(fn.contentHash.length).toBe(64); // SHA256 hex
98
- }
99
- });
100
- (0, vitest_1.it)("same source code produces same hash (deterministic)", () => {
101
- const fns1 = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
102
- const fns2 = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
103
- (0, vitest_1.expect)(fns1.map((f) => f.contentHash)).toEqual(fns2.map((f) => f.contentHash));
104
- });
105
- });
106
- });
@@ -1,46 +0,0 @@
1
- import Database from "better-sqlite3";
2
- /**
3
- * Creates an in-memory SQLite database with the current ophan schema.
4
- * Use for merge, import, GC, and findMissingHashes tests.
5
- */
6
- export declare function createTestDb(): Database.Database;
7
- /**
8
- * Creates an in-memory DB with the OLD (pre-analysis-type) schema.
9
- * Used to test migrateToAnalysisTypes().
10
- */
11
- export declare function createLegacyTestDb(): Database.Database;
12
- /** Returns JSON string for a documentation analysis payload. */
13
- export declare function makeDocAnalysisJson(overrides?: Partial<{
14
- description: string;
15
- params: {
16
- name: string;
17
- type: string;
18
- description: string;
19
- }[];
20
- returns: {
21
- type: string;
22
- description: string;
23
- };
24
- }>): string;
25
- /** Returns JSON string for a security analysis payload. */
26
- export declare function makeSecAnalysisJson(overrides?: Partial<{
27
- dataTags: string[];
28
- securityFlags: string[];
29
- }>): string;
30
- /** Inserts a matched pair of documentation + security rows for a content_hash. */
31
- export declare function insertAnalysisPair(db: Database.Database, contentHash: string, options?: {
32
- language?: string;
33
- entityType?: string;
34
- syncedAt?: number | null;
35
- lastSeenAt?: number;
36
- createdAt?: number;
37
- doc?: Parameters<typeof makeDocAnalysisJson>[0];
38
- sec?: Parameters<typeof makeSecAnalysisJson>[0];
39
- }): void;
40
- /** Inserts a file_functions row. */
41
- export declare function insertFileFunction(db: Database.Database, filePath: string, functionName: string, contentHash: string, options?: {
42
- mtime?: number;
43
- language?: string;
44
- entityType?: string;
45
- }): void;
46
- //# sourceMappingURL=test-utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC;;;GAGG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAAC,QAAQ,CAsDhD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,QAAQ,CAAC,QAAQ,CA2CtD;AAED,gEAAgE;AAChE,wBAAgB,mBAAmB,CACjC,SAAS,GAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD,CAAM,GACN,MAAM,CAQR;AAED,2DAA2D;AAC3D,wBAAgB,mBAAmB,CACjC,SAAS,GAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB,CAAM,GACN,MAAM,CAKR;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,GAAG,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;CAC5C,GACL,IAAI,CAmCN;AAED,oCAAoC;AACpC,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE;IACP,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CAChB,GACL,IAAI,CAYN"}
@@ -1,141 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createTestDb = createTestDb;
7
- exports.createLegacyTestDb = createLegacyTestDb;
8
- exports.makeDocAnalysisJson = makeDocAnalysisJson;
9
- exports.makeSecAnalysisJson = makeSecAnalysisJson;
10
- exports.insertAnalysisPair = insertAnalysisPair;
11
- exports.insertFileFunction = insertFileFunction;
12
- const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
13
- /**
14
- * Creates an in-memory SQLite database with the current ophan schema.
15
- * Use for merge, import, GC, and findMissingHashes tests.
16
- */
17
- function createTestDb() {
18
- const db = new better_sqlite3_1.default(":memory:");
19
- db.exec(`
20
- CREATE TABLE function_analysis (
21
- content_hash TEXT NOT NULL,
22
- analysis_type TEXT NOT NULL,
23
- analysis JSON NOT NULL,
24
- model_version TEXT NOT NULL,
25
- schema_version INTEGER NOT NULL DEFAULT 1,
26
- created_at INTEGER NOT NULL,
27
- last_seen_at INTEGER NOT NULL,
28
- language TEXT NOT NULL DEFAULT 'typescript',
29
- entity_type TEXT NOT NULL DEFAULT 'function',
30
- synced_at INTEGER,
31
- PRIMARY KEY (content_hash, analysis_type)
32
- )
33
- `);
34
- db.exec(`
35
- CREATE TABLE file_functions (
36
- file_path TEXT NOT NULL,
37
- function_name TEXT NOT NULL,
38
- content_hash TEXT NOT NULL,
39
- file_mtime INTEGER NOT NULL,
40
- language TEXT NOT NULL DEFAULT 'typescript',
41
- entity_type TEXT NOT NULL DEFAULT 'function'
42
- )
43
- `);
44
- db.exec(`
45
- CREATE TABLE function_gc (
46
- content_hash TEXT NOT NULL,
47
- analysis_type TEXT,
48
- gc_at INTEGER NOT NULL,
49
- synced_at INTEGER
50
- )
51
- `);
52
- db.exec(`
53
- CREATE TABLE sync_meta (
54
- key TEXT PRIMARY KEY,
55
- value TEXT NOT NULL
56
- )
57
- `);
58
- db.exec("CREATE INDEX idx_file_functions_path ON file_functions(file_path)");
59
- db.exec("CREATE INDEX idx_file_functions_hash ON file_functions(content_hash)");
60
- return db;
61
- }
62
- /**
63
- * Creates an in-memory DB with the OLD (pre-analysis-type) schema.
64
- * Used to test migrateToAnalysisTypes().
65
- */
66
- function createLegacyTestDb() {
67
- const db = new better_sqlite3_1.default(":memory:");
68
- db.exec(`
69
- CREATE TABLE function_analysis (
70
- content_hash TEXT PRIMARY KEY,
71
- analysis JSON NOT NULL,
72
- model_version TEXT NOT NULL,
73
- created_at INTEGER NOT NULL,
74
- last_seen_at INTEGER NOT NULL,
75
- language TEXT,
76
- entity_type TEXT,
77
- synced_at INTEGER
78
- )
79
- `);
80
- db.exec(`
81
- CREATE TABLE file_functions (
82
- file_path TEXT NOT NULL,
83
- function_name TEXT NOT NULL,
84
- content_hash TEXT NOT NULL,
85
- file_mtime INTEGER NOT NULL,
86
- language TEXT NOT NULL DEFAULT 'typescript',
87
- entity_type TEXT NOT NULL DEFAULT 'function'
88
- )
89
- `);
90
- db.exec(`
91
- CREATE TABLE function_gc (
92
- content_hash TEXT NOT NULL,
93
- gc_at INTEGER NOT NULL,
94
- synced_at INTEGER
95
- )
96
- `);
97
- db.exec(`
98
- CREATE TABLE sync_meta (
99
- key TEXT PRIMARY KEY,
100
- value TEXT NOT NULL
101
- )
102
- `);
103
- return db;
104
- }
105
- /** Returns JSON string for a documentation analysis payload. */
106
- function makeDocAnalysisJson(overrides = {}) {
107
- return JSON.stringify({
108
- description: overrides.description ?? "Test function description",
109
- params: overrides.params ?? [
110
- { name: "x", type: "number", description: "input" },
111
- ],
112
- returns: overrides.returns ?? { type: "number", description: "output" },
113
- });
114
- }
115
- /** Returns JSON string for a security analysis payload. */
116
- function makeSecAnalysisJson(overrides = {}) {
117
- return JSON.stringify({
118
- dataTags: overrides.dataTags ?? ["internal"],
119
- securityFlags: overrides.securityFlags ?? [],
120
- });
121
- }
122
- /** Inserts a matched pair of documentation + security rows for a content_hash. */
123
- function insertAnalysisPair(db, contentHash, options = {}) {
124
- const now = Math.floor(Date.now() / 1000);
125
- const language = options.language ?? "typescript";
126
- const entityType = options.entityType ?? "function";
127
- const createdAt = options.createdAt ?? now;
128
- const lastSeenAt = options.lastSeenAt ?? now;
129
- const syncedAt = options.syncedAt ?? null;
130
- db.prepare(`INSERT INTO function_analysis
131
- (content_hash, analysis_type, analysis, model_version, schema_version, created_at, last_seen_at, language, entity_type, synced_at)
132
- VALUES (?, 'documentation', ?, 'test-model', 1, ?, ?, ?, ?, ?)`).run(contentHash, makeDocAnalysisJson(options.doc), createdAt, lastSeenAt, language, entityType, syncedAt);
133
- db.prepare(`INSERT INTO function_analysis
134
- (content_hash, analysis_type, analysis, model_version, schema_version, created_at, last_seen_at, language, entity_type, synced_at)
135
- VALUES (?, 'security', ?, 'test-model', 1, ?, ?, ?, ?, ?)`).run(contentHash, makeSecAnalysisJson(options.sec), createdAt, lastSeenAt, language, entityType, syncedAt);
136
- }
137
- /** Inserts a file_functions row. */
138
- function insertFileFunction(db, filePath, functionName, contentHash, options = {}) {
139
- db.prepare(`INSERT INTO file_functions (file_path, function_name, content_hash, file_mtime, language, entity_type)
140
- VALUES (?, ?, ?, ?, ?, ?)`).run(filePath, functionName, contentHash, options.mtime ?? Date.now(), options.language ?? "typescript", options.entityType ?? "function");
141
- }