@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 +56 -93
- package/package.json +4 -2
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -492
- package/dist/parsers/python.test.d.ts +0 -2
- package/dist/parsers/python.test.d.ts.map +0 -1
- package/dist/parsers/python.test.js +0 -96
- package/dist/parsers/typescript.test.d.ts +0 -2
- package/dist/parsers/typescript.test.d.ts.map +0 -1
- package/dist/parsers/typescript.test.js +0 -106
- package/dist/test-utils.d.ts +0 -46
- package/dist/test-utils.d.ts.map +0 -1
- package/dist/test-utils.js +0 -141
package/README.md
CHANGED
|
@@ -1,107 +1,70 @@
|
|
|
1
1
|
# @ophan/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
34
|
+
## Usage
|
|
90
35
|
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
61
|
+
## Resources
|
|
98
62
|
|
|
99
|
-
|
|
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
|
-
##
|
|
68
|
+
## License
|
|
102
69
|
|
|
103
|
-
|
|
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.
|
|
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": [
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
12
14
|
"publishConfig": {
|
|
13
15
|
"main": "./dist/index.js",
|
|
14
16
|
"types": "./dist/index.d.ts",
|
package/dist/index.test.d.ts
DELETED
package/dist/index.test.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
|
package/dist/index.test.js
DELETED
|
@@ -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 +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 +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
|
-
});
|
package/dist/test-utils.d.ts
DELETED
|
@@ -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
|
package/dist/test-utils.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/test-utils.js
DELETED
|
@@ -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
|
-
}
|