@specverse/engines 6.0.6 → 6.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyse-prepass/adapters/index.d.ts +9 -0
- package/dist/analyse-prepass/adapters/index.d.ts.map +1 -0
- package/dist/analyse-prepass/adapters/index.js +9 -0
- package/dist/analyse-prepass/adapters/index.js.map +1 -0
- package/dist/analyse-prepass/adapters/typescript-prisma.d.ts +67 -0
- package/dist/analyse-prepass/adapters/typescript-prisma.d.ts.map +1 -0
- package/dist/analyse-prepass/adapters/typescript-prisma.js +167 -0
- package/dist/analyse-prepass/adapters/typescript-prisma.js.map +1 -0
- package/dist/analyse-prepass/backends/codegraph.d.ts +56 -0
- package/dist/analyse-prepass/backends/codegraph.d.ts.map +1 -0
- package/dist/analyse-prepass/backends/codegraph.js +303 -0
- package/dist/analyse-prepass/backends/codegraph.js.map +1 -0
- package/dist/analyse-prepass/backends/grep-only.d.ts +33 -0
- package/dist/analyse-prepass/backends/grep-only.d.ts.map +1 -0
- package/dist/analyse-prepass/backends/grep-only.js +377 -0
- package/dist/analyse-prepass/backends/grep-only.js.map +1 -0
- package/dist/analyse-prepass/backends/index.d.ts +20 -0
- package/dist/analyse-prepass/backends/index.d.ts.map +1 -0
- package/dist/analyse-prepass/backends/index.js +27 -0
- package/dist/analyse-prepass/backends/index.js.map +1 -0
- package/dist/analyse-prepass/backends/method-patterns.d.ts +27 -0
- package/dist/analyse-prepass/backends/method-patterns.d.ts.map +1 -0
- package/dist/analyse-prepass/backends/method-patterns.js +177 -0
- package/dist/analyse-prepass/backends/method-patterns.js.map +1 -0
- package/dist/analyse-prepass/backends/walk.d.ts +7 -0
- package/dist/analyse-prepass/backends/walk.d.ts.map +1 -0
- package/dist/analyse-prepass/backends/walk.js +105 -0
- package/dist/analyse-prepass/backends/walk.js.map +1 -0
- package/dist/analyse-prepass/index.d.ts +76 -0
- package/dist/analyse-prepass/index.d.ts.map +1 -0
- package/dist/analyse-prepass/index.js +114 -0
- package/dist/analyse-prepass/index.js.map +1 -0
- package/dist/analyse-prepass/interface.d.ts +222 -0
- package/dist/analyse-prepass/interface.d.ts.map +1 -0
- package/dist/analyse-prepass/interface.js +20 -0
- package/dist/analyse-prepass/interface.js.map +1 -0
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +27 -5
- package/dist/inference/index.js.map +1 -1
- package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +25 -4
- package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +43 -5
- package/package.json +5 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecVerse adapters — translate StructuralPrepass output into SpecVerse
|
|
3
|
+
* vocabulary (entities / relationships / lifecycles / controllers / etc.).
|
|
4
|
+
*
|
|
5
|
+
* Each adapter is per-framework (NOT per-language). Most languages have
|
|
6
|
+
* 2-4 dominant ORMs / frameworks; ~10 adapters cover ~90% of analyse cases.
|
|
7
|
+
*/
|
|
8
|
+
export { extractTypeScriptPrisma, type PrismaFacts, type PrismaEntity, type PrismaField, type PrismaRelation, } from './typescript-prisma.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,uBAAuB,EACvB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecVerse adapters — translate StructuralPrepass output into SpecVerse
|
|
3
|
+
* vocabulary (entities / relationships / lifecycles / controllers / etc.).
|
|
4
|
+
*
|
|
5
|
+
* Each adapter is per-framework (NOT per-language). Most languages have
|
|
6
|
+
* 2-4 dominant ORMs / frameworks; ~10 adapters cover ~90% of analyse cases.
|
|
7
|
+
*/
|
|
8
|
+
export { extractTypeScriptPrisma, } from './typescript-prisma.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,uBAAuB,GAKxB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript + Prisma adapter — the first SpecVerse-vocabulary translator.
|
|
3
|
+
*
|
|
4
|
+
* Reads `schema.prisma` files surfaced by the structural pre-pass and
|
|
5
|
+
* extracts SpecVerse facts:
|
|
6
|
+
* - Models → entities
|
|
7
|
+
* - `@relation` declarations → relationships (belongsTo / hasMany)
|
|
8
|
+
* - enum declarations → lifecycle state candidates (when paired with a
|
|
9
|
+
* status-typed field on a model)
|
|
10
|
+
* - Field types + modifiers (@id, @unique, @default) → SpecVerse attributes
|
|
11
|
+
*
|
|
12
|
+
* Why Prisma first: SpecVerse realize emits Prisma in its default backend
|
|
13
|
+
* stack, so every Class 1 corpus case (poll-app, full-stack-template,
|
|
14
|
+
* steps-example) uses Prisma. Prisma's vocabulary is also closer to
|
|
15
|
+
* SpecVerse than other ORMs — minimal semantic translation needed.
|
|
16
|
+
*
|
|
17
|
+
* Adapter responsibilities (per the structural-prepass proposal):
|
|
18
|
+
* - Consume the StructuralPrepass interface (file listings, source text)
|
|
19
|
+
* - NOT parse arbitrary code (that's the backend's job — tree-sitter)
|
|
20
|
+
* - Produce SpecVerse-shaped facts that the analyse prompt can consume
|
|
21
|
+
*/
|
|
22
|
+
import type { StructuralPrepass } from '../interface.js';
|
|
23
|
+
export interface PrismaField {
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
required: boolean;
|
|
27
|
+
unique: boolean;
|
|
28
|
+
default?: string;
|
|
29
|
+
isId: boolean;
|
|
30
|
+
isList: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface PrismaRelation {
|
|
33
|
+
fromModel: string;
|
|
34
|
+
toModel: string;
|
|
35
|
+
/** belongsTo if the FK lives on this side; hasMany if inverse */
|
|
36
|
+
type: 'belongsTo' | 'hasMany' | 'hasOne';
|
|
37
|
+
cascade: boolean;
|
|
38
|
+
/** The field name carrying the relation (for belongsTo) or the inverse field (for hasMany) */
|
|
39
|
+
fieldName: string;
|
|
40
|
+
}
|
|
41
|
+
export interface PrismaEntity {
|
|
42
|
+
name: string;
|
|
43
|
+
filePath: string;
|
|
44
|
+
fields: PrismaField[];
|
|
45
|
+
/** Lifecycle state literals from a status field's enum, if detected. */
|
|
46
|
+
lifecycleStates?: string[];
|
|
47
|
+
/** The status field name (if a lifecycle was detected). */
|
|
48
|
+
lifecycleField?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface PrismaFacts {
|
|
51
|
+
entities: PrismaEntity[];
|
|
52
|
+
relationships: PrismaRelation[];
|
|
53
|
+
/** Enum declarations in source — keyed by name. Used by the lifecycle detector. */
|
|
54
|
+
enums: Record<string, string[]>;
|
|
55
|
+
/** Schema files found. */
|
|
56
|
+
schemaFiles: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run the Prisma adapter against a structural pre-pass.
|
|
60
|
+
*
|
|
61
|
+
* The adapter relies on `prepass.fileSourceText()` to read schema.prisma
|
|
62
|
+
* directly — it doesn't need any of the backend's call-graph or symbol
|
|
63
|
+
* indexing for Prisma extraction. Prisma's schema syntax is small enough
|
|
64
|
+
* that regex over the raw text is the right tool.
|
|
65
|
+
*/
|
|
66
|
+
export declare function extractTypeScriptPrisma(prepass: StructuralPrepass): Promise<PrismaFacts>;
|
|
67
|
+
//# sourceMappingURL=typescript-prisma.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typescript-prisma.d.ts","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/typescript-prisma.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,8FAA8F;IAC9F,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,aAAa,EAAE,cAAc,EAAE,CAAC;IAChC,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,0BAA0B;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAyG9F"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the Prisma adapter against a structural pre-pass.
|
|
3
|
+
*
|
|
4
|
+
* The adapter relies on `prepass.fileSourceText()` to read schema.prisma
|
|
5
|
+
* directly — it doesn't need any of the backend's call-graph or symbol
|
|
6
|
+
* indexing for Prisma extraction. Prisma's schema syntax is small enough
|
|
7
|
+
* that regex over the raw text is the right tool.
|
|
8
|
+
*/
|
|
9
|
+
export async function extractTypeScriptPrisma(prepass) {
|
|
10
|
+
const schemaFiles = await prepass.listFiles({ glob: '**/schema.prisma' });
|
|
11
|
+
if (schemaFiles.length === 0) {
|
|
12
|
+
return { entities: [], relationships: [], enums: {}, schemaFiles: [] };
|
|
13
|
+
}
|
|
14
|
+
const enums = {};
|
|
15
|
+
const entities = [];
|
|
16
|
+
const relationships = [];
|
|
17
|
+
for (const file of schemaFiles) {
|
|
18
|
+
const text = await prepass.fileSourceText(file);
|
|
19
|
+
// Extract enum declarations
|
|
20
|
+
for (const match of text.matchAll(/enum\s+(\w+)\s*\{([^}]+)\}/g)) {
|
|
21
|
+
const name = match[1];
|
|
22
|
+
const body = match[2];
|
|
23
|
+
const values = body
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((l) => l.trim())
|
|
26
|
+
.filter((l) => l && !l.startsWith('//'))
|
|
27
|
+
.map((l) => l.split(/\s+/)[0])
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
enums[name] = values;
|
|
30
|
+
}
|
|
31
|
+
// Extract model declarations
|
|
32
|
+
for (const match of text.matchAll(/model\s+(\w+)\s*\{([^}]+)\}/g)) {
|
|
33
|
+
const modelName = match[1];
|
|
34
|
+
const body = match[2];
|
|
35
|
+
const fields = parseFields(body);
|
|
36
|
+
// Detect lifecycle: a field of an enum type — the enum's values are
|
|
37
|
+
// candidate states. Common SpecVerse pattern: `status SomeStatus` field
|
|
38
|
+
// with a corresponding `enum SomeStatus { ... }`.
|
|
39
|
+
let lifecycleStates;
|
|
40
|
+
let lifecycleField;
|
|
41
|
+
for (const f of fields) {
|
|
42
|
+
// The field type may itself be an enum we extracted above
|
|
43
|
+
const typeName = f.type.replace(/\?$/, ''); // strip optional marker
|
|
44
|
+
if (enums[typeName]) {
|
|
45
|
+
lifecycleStates = enums[typeName];
|
|
46
|
+
lifecycleField = f.name;
|
|
47
|
+
break; // Take first enum-typed field; canonical is `status`
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
entities.push({
|
|
51
|
+
name: modelName,
|
|
52
|
+
filePath: file,
|
|
53
|
+
fields,
|
|
54
|
+
lifecycleStates,
|
|
55
|
+
lifecycleField,
|
|
56
|
+
});
|
|
57
|
+
// Extract relationships from this model's fields
|
|
58
|
+
for (const rel of parseRelationships(modelName, body, fields)) {
|
|
59
|
+
relationships.push(rel);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Second pass: derive `hasMany` from `belongsTo` declarations on the inverse
|
|
64
|
+
// side (Prisma schemas typically declare both sides explicitly). Crucially,
|
|
65
|
+
// we PROPAGATE cascade flags from the belongsTo side (where Prisma's
|
|
66
|
+
// `onDelete: Cascade` syntactically lives) up to the hasMany side (where
|
|
67
|
+
// SpecVerse semantics expect the cascade decision to live — "deleting the
|
|
68
|
+
// parent cascades to its children").
|
|
69
|
+
const declaredHasMany = new Set(relationships.filter((r) => r.type === 'hasMany').map((r) => `${r.fromModel}→${r.toModel}`));
|
|
70
|
+
for (const ent of entities) {
|
|
71
|
+
for (const f of ent.fields) {
|
|
72
|
+
if (!f.isList)
|
|
73
|
+
continue;
|
|
74
|
+
// Is this a list-typed field whose type is another model?
|
|
75
|
+
const typeName = f.type.replace(/\[\]$/, '').replace(/\?$/, '');
|
|
76
|
+
const isModel = entities.some((e) => e.name === typeName);
|
|
77
|
+
if (!isModel)
|
|
78
|
+
continue;
|
|
79
|
+
const key = `${ent.name}→${typeName}`;
|
|
80
|
+
if (!declaredHasMany.has(key)) {
|
|
81
|
+
// Find the inverse belongsTo (if declared) — it carries the cascade flag
|
|
82
|
+
// that we want to surface on the hasMany side.
|
|
83
|
+
const inverse = relationships.find((r) => r.type === 'belongsTo' && r.fromModel === typeName && r.toModel === ent.name);
|
|
84
|
+
relationships.push({
|
|
85
|
+
fromModel: ent.name,
|
|
86
|
+
toModel: typeName,
|
|
87
|
+
type: 'hasMany',
|
|
88
|
+
cascade: inverse?.cascade ?? false,
|
|
89
|
+
fieldName: f.name,
|
|
90
|
+
});
|
|
91
|
+
declaredHasMany.add(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Third pass: clear cascade from belongsTo entries (in SpecVerse vocabulary,
|
|
96
|
+
// cascade is a property of the parent→child edge, not the child→parent FK
|
|
97
|
+
// edge). The flag has already been propagated to hasMany above.
|
|
98
|
+
for (const r of relationships) {
|
|
99
|
+
if (r.type === 'belongsTo')
|
|
100
|
+
r.cascade = false;
|
|
101
|
+
}
|
|
102
|
+
return { entities, relationships, enums, schemaFiles };
|
|
103
|
+
}
|
|
104
|
+
// ────────────────────────────────────────────────────────────────────
|
|
105
|
+
// Prisma-syntax parsers (regex-based; sufficient for schema.prisma)
|
|
106
|
+
// ────────────────────────────────────────────────────────────────────
|
|
107
|
+
function parseFields(modelBody) {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const rawLine of modelBody.split(/\r?\n/)) {
|
|
110
|
+
const line = rawLine.trim();
|
|
111
|
+
if (!line || line.startsWith('//') || line.startsWith('@@'))
|
|
112
|
+
continue;
|
|
113
|
+
// A field line: `name Type[?] [modifiers...]`
|
|
114
|
+
// Skip relation-only lines (they have @relation annotation typically on a separate line — handled below)
|
|
115
|
+
const match = line.match(/^(\w+)\s+(\w+(?:\[\])?(?:\?)?)\s*(.*)$/);
|
|
116
|
+
if (!match)
|
|
117
|
+
continue;
|
|
118
|
+
const [, name, fullType, rest] = match;
|
|
119
|
+
const isList = fullType.endsWith('[]');
|
|
120
|
+
const isOptional = fullType.endsWith('?');
|
|
121
|
+
const baseType = fullType.replace(/\[\]/, '').replace(/\?$/, '');
|
|
122
|
+
out.push({
|
|
123
|
+
name,
|
|
124
|
+
type: fullType,
|
|
125
|
+
required: !isOptional && !isList, // List fields are intrinsically optional in Prisma semantics
|
|
126
|
+
unique: /@unique\b/.test(rest),
|
|
127
|
+
default: extractDefault(rest),
|
|
128
|
+
isId: /@id\b/.test(rest),
|
|
129
|
+
isList,
|
|
130
|
+
});
|
|
131
|
+
// Use baseType here to detect what we're actually pointing at — it's a hint for callers
|
|
132
|
+
void baseType;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function extractDefault(rest) {
|
|
137
|
+
const match = rest.match(/@default\(([^)]+)\)/);
|
|
138
|
+
return match ? match[1].trim() : undefined;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Detect `belongsTo` relationships declared via `@relation(fields: [...], references: [...])`.
|
|
142
|
+
* The model containing such a declaration owns the foreign key — it `belongsTo` the target.
|
|
143
|
+
*/
|
|
144
|
+
function parseRelationships(modelName, modelBody, fields) {
|
|
145
|
+
const out = [];
|
|
146
|
+
// Match: `field TargetType[?] @relation(fields: [...], references: [...], onDelete: Cascade)`
|
|
147
|
+
const relationRegex = /^\s*(\w+)\s+(\w+)(\?)?\s+@relation\(([^)]+)\)/gm;
|
|
148
|
+
for (const match of modelBody.matchAll(relationRegex)) {
|
|
149
|
+
const [, fieldName, targetType] = match;
|
|
150
|
+
const annotation = match[4];
|
|
151
|
+
// It's a belongsTo — the field's type is the target model
|
|
152
|
+
const cascade = /onDelete:\s*Cascade/.test(annotation);
|
|
153
|
+
// Skip self-referential or array-typed entries (those are hasMany, not belongsTo)
|
|
154
|
+
const f = fields.find((x) => x.name === fieldName);
|
|
155
|
+
if (f?.isList)
|
|
156
|
+
continue;
|
|
157
|
+
out.push({
|
|
158
|
+
fromModel: modelName,
|
|
159
|
+
toModel: targetType,
|
|
160
|
+
type: 'belongsTo',
|
|
161
|
+
cascade,
|
|
162
|
+
fieldName,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=typescript-prisma.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typescript-prisma.js","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/typescript-prisma.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { StructuralPrepass, Symbol, Import, FileFilter, ClassFilter, MethodFilter, InterfaceFilter, IndexResult, MethodFactSheet } from '../interface.js';
|
|
2
|
+
export interface CodeGraphBackendOptions {
|
|
3
|
+
/** Path to the codegraph binary. Auto-detected if omitted. */
|
|
4
|
+
codegraphPath?: string;
|
|
5
|
+
/** Path to the sqlite3 binary. Auto-detected if omitted. */
|
|
6
|
+
sqlite3Path?: string;
|
|
7
|
+
/** Skip running `codegraph init` + `codegraph index` if .codegraph/ already exists. */
|
|
8
|
+
reuseExistingIndex?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class CodeGraphBackend implements StructuralPrepass {
|
|
11
|
+
private sourceDir;
|
|
12
|
+
private dbPath;
|
|
13
|
+
private codegraphPath;
|
|
14
|
+
private sqlite3Path;
|
|
15
|
+
private reuseExistingIndex;
|
|
16
|
+
capabilities: {
|
|
17
|
+
callGraph: boolean;
|
|
18
|
+
crossFileResolution: boolean;
|
|
19
|
+
componentClustering: boolean;
|
|
20
|
+
fullTextSearch: boolean;
|
|
21
|
+
fileWatching: boolean;
|
|
22
|
+
};
|
|
23
|
+
constructor(options?: CodeGraphBackendOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Check if CodeGraph + sqlite3 are available. Use this from the adapter
|
|
26
|
+
* selector to decide whether to fall back to grep-only.
|
|
27
|
+
*/
|
|
28
|
+
static isAvailable(): boolean;
|
|
29
|
+
init(sourceDir: string): Promise<void>;
|
|
30
|
+
index(): Promise<IndexResult>;
|
|
31
|
+
listFiles(filter?: FileFilter): Promise<string[]>;
|
|
32
|
+
listClasses(filter?: ClassFilter): Promise<Symbol[]>;
|
|
33
|
+
listMethods(filter?: MethodFilter): Promise<Symbol[]>;
|
|
34
|
+
listInterfaces(filter?: InterfaceFilter): Promise<Symbol[]>;
|
|
35
|
+
listImports(file: string): Promise<Import[]>;
|
|
36
|
+
fileSourceText(file: string): Promise<string>;
|
|
37
|
+
callers(symbol: string): Promise<Symbol[]>;
|
|
38
|
+
callees(symbol: string): Promise<Symbol[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Per-method fact sheet via SQL (for the call graph) + regex over the body
|
|
41
|
+
* (for everything else: emits, dbWrites, externalCalls, throws, async, branches).
|
|
42
|
+
*
|
|
43
|
+
* The SQL piece is what justifies the CodeGraph backend over grep-only here —
|
|
44
|
+
* tree-sitter resolved cross-file calls deterministically, while a regex over
|
|
45
|
+
* a single method body would only see same-file references.
|
|
46
|
+
*/
|
|
47
|
+
getMethodDetails(qualifiedName: string): Promise<MethodFactSheet | null>;
|
|
48
|
+
/** Run a SQL query; expect 0 or 1 row. Throws if more. */
|
|
49
|
+
private queryOne;
|
|
50
|
+
/** Run a SQL query; return all rows as objects (parsed from JSON). */
|
|
51
|
+
private queryAll;
|
|
52
|
+
private rowToSymbol;
|
|
53
|
+
private escape;
|
|
54
|
+
private detectBinary;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=codegraph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegraph.d.ts","sourceRoot":"","sources":["../../../src/analyse-prepass/backends/codegraph.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,UAAU,EACV,WAAW,EACX,YAAY,EACZ,eAAe,EACf,WAAW,EACX,eAAe,EAChB,MAAM,iBAAiB,CAAC;AAyCzB,MAAM,WAAW,uBAAuB;IACtC,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,qBAAa,gBAAiB,YAAW,iBAAiB;IACxD,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,kBAAkB,CAAU;IAEpC,YAAY;;;;;;MAMV;gBAEU,OAAO,GAAE,uBAA4B;IAMjD;;;OAGG;IACH,MAAM,CAAC,WAAW,IAAI,OAAO;IAUvB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBtC,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;IAsB7B,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAcjD,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAUpD,WAAW,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAcrD,cAAc,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAU3D,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAc5C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAW1C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAWhD;;;;;;;OAOG;IACG,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAgD9E,0DAA0D;IAC1D,OAAO,CAAC,QAAQ;IAMhB,sEAAsE;IACtE,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,YAAY;CASrB"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeGraph backend — `StructuralPrepass` against `@colbymchenry/codegraph`.
|
|
3
|
+
*
|
|
4
|
+
* Uses CodeGraph's CLI for init/index, and queries its SQLite directly via
|
|
5
|
+
* the `sqlite3` command-line tool (preinstalled on macOS / Linux / Windows).
|
|
6
|
+
* Falls back to grep-only style scanning if CodeGraph isn't installed.
|
|
7
|
+
*
|
|
8
|
+
* Design rationale: CodeGraph ships a tree-sitter-based indexer for 19+
|
|
9
|
+
* languages with cross-file resolution + call-graph + FTS, all stored in
|
|
10
|
+
* SQLite. We get tree-sitter coverage for free; we just translate its
|
|
11
|
+
* vocabulary (class / method / function / interface) into our SpecVerse
|
|
12
|
+
* vocabulary downstream in the adapter layer.
|
|
13
|
+
*
|
|
14
|
+
* Relies on:
|
|
15
|
+
* - `codegraph` CLI on PATH (or installable via `npm i -g @colbymchenry/codegraph`)
|
|
16
|
+
* - `sqlite3` CLI on PATH (preinstalled on most Unix systems)
|
|
17
|
+
*/
|
|
18
|
+
import { execSync, execFileSync } from 'child_process';
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { walkSourceTree, detectLanguage, globToRegex } from './walk.js';
|
|
22
|
+
import { extractEmits, extractDbWrites, extractExternalCalls, extractThrows, extractAsyncBoundaries, countBranchPoints, sliceBody, } from './method-patterns.js';
|
|
23
|
+
/** Map CodeGraph's `nodes.kind` enum to our Symbol['kind']. */
|
|
24
|
+
function mapKind(cgKind) {
|
|
25
|
+
switch (cgKind) {
|
|
26
|
+
case 'class': return 'class';
|
|
27
|
+
case 'function': return 'function';
|
|
28
|
+
case 'method': return 'method';
|
|
29
|
+
case 'interface': return 'interface';
|
|
30
|
+
case 'type_alias': return 'type_alias';
|
|
31
|
+
case 'constant': return 'constant';
|
|
32
|
+
// CodeGraph doesn't always emit 'enum' as a separate kind; treat as type_alias
|
|
33
|
+
default: return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class CodeGraphBackend {
|
|
37
|
+
sourceDir = '';
|
|
38
|
+
dbPath = '';
|
|
39
|
+
codegraphPath;
|
|
40
|
+
sqlite3Path;
|
|
41
|
+
reuseExistingIndex;
|
|
42
|
+
capabilities = {
|
|
43
|
+
callGraph: true,
|
|
44
|
+
crossFileResolution: true,
|
|
45
|
+
componentClustering: false, // CodeGraph doesn't have Leiden
|
|
46
|
+
fullTextSearch: true, // FTS5 in SQLite
|
|
47
|
+
fileWatching: true, // OS file watchers + 2s debounce
|
|
48
|
+
};
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.codegraphPath = options.codegraphPath ?? this.detectBinary('codegraph');
|
|
51
|
+
this.sqlite3Path = options.sqlite3Path ?? this.detectBinary('sqlite3');
|
|
52
|
+
this.reuseExistingIndex = options.reuseExistingIndex ?? false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if CodeGraph + sqlite3 are available. Use this from the adapter
|
|
56
|
+
* selector to decide whether to fall back to grep-only.
|
|
57
|
+
*/
|
|
58
|
+
static isAvailable() {
|
|
59
|
+
try {
|
|
60
|
+
execSync('codegraph --version', { stdio: 'ignore', timeout: 5000 });
|
|
61
|
+
execSync('sqlite3 --version', { stdio: 'ignore', timeout: 5000 });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async init(sourceDir) {
|
|
69
|
+
this.sourceDir = sourceDir;
|
|
70
|
+
this.dbPath = join(sourceDir, '.codegraph', 'codegraph.db');
|
|
71
|
+
if (this.reuseExistingIndex && existsSync(this.dbPath))
|
|
72
|
+
return;
|
|
73
|
+
// CodeGraph's `init` is interactive (asks about Claude integration);
|
|
74
|
+
// we feed "n" via stdin to skip the interactive setup.
|
|
75
|
+
if (!existsSync(join(sourceDir, '.codegraph'))) {
|
|
76
|
+
execSync(`echo "n" | ${this.codegraphPath} init`, {
|
|
77
|
+
cwd: sourceDir,
|
|
78
|
+
stdio: 'ignore',
|
|
79
|
+
timeout: 30_000,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async index() {
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
if (this.reuseExistingIndex && existsSync(this.dbPath)) {
|
|
86
|
+
// Trust existing index. Stats from DB.
|
|
87
|
+
const stats = this.queryOne(`SELECT (SELECT COUNT(*) FROM nodes WHERE kind='file') AS files,
|
|
88
|
+
(SELECT COUNT(*) FROM nodes WHERE kind != 'file') AS symbols`);
|
|
89
|
+
return { ...stats, durationMs: Date.now() - start };
|
|
90
|
+
}
|
|
91
|
+
execSync(`${this.codegraphPath} index`, {
|
|
92
|
+
cwd: this.sourceDir,
|
|
93
|
+
stdio: 'ignore',
|
|
94
|
+
timeout: 600_000, // 10 min for large repos
|
|
95
|
+
});
|
|
96
|
+
const stats = this.queryOne(`SELECT (SELECT COUNT(*) FROM nodes WHERE kind='file') AS files,
|
|
97
|
+
(SELECT COUNT(*) FROM nodes WHERE kind != 'file') AS symbols`);
|
|
98
|
+
return { ...stats, durationMs: Date.now() - start };
|
|
99
|
+
}
|
|
100
|
+
async listFiles(filter) {
|
|
101
|
+
// listFiles uses an FS walk (not the index) because adapters need to find
|
|
102
|
+
// arbitrary file types (schema.prisma, package.json, config files) — and
|
|
103
|
+
// CodeGraph only indexes its 19 supported languages, missing those.
|
|
104
|
+
let result = walkSourceTree(this.sourceDir);
|
|
105
|
+
if (filter?.dir)
|
|
106
|
+
result = result.filter((f) => f.startsWith(filter.dir));
|
|
107
|
+
if (filter?.lang)
|
|
108
|
+
result = result.filter((f) => detectLanguage(f) === filter.lang);
|
|
109
|
+
if (filter?.glob) {
|
|
110
|
+
const globRegex = globToRegex(filter.glob);
|
|
111
|
+
result = result.filter((f) => globRegex.test(f));
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
async listClasses(filter) {
|
|
116
|
+
let sql = `SELECT * FROM nodes WHERE kind='class'`;
|
|
117
|
+
if (filter?.dir)
|
|
118
|
+
sql += ` AND file_path LIKE '${this.escape(filter.dir)}%'`;
|
|
119
|
+
sql += ` ORDER BY file_path, start_line`;
|
|
120
|
+
const rows = this.queryAll(sql);
|
|
121
|
+
return rows
|
|
122
|
+
.filter((r) => !filter?.namePattern || filter.namePattern.test(r.name))
|
|
123
|
+
.map((r) => this.rowToSymbol(r));
|
|
124
|
+
}
|
|
125
|
+
async listMethods(filter) {
|
|
126
|
+
let sql = `SELECT * FROM nodes WHERE kind='method'`;
|
|
127
|
+
const conditions = [];
|
|
128
|
+
if (filter?.class)
|
|
129
|
+
conditions.push(`qualified_name LIKE '${this.escape(filter.class)}%'`);
|
|
130
|
+
if (filter?.nameIn && filter.nameIn.length > 0) {
|
|
131
|
+
const list = filter.nameIn.map((n) => `'${this.escape(n)}'`).join(',');
|
|
132
|
+
conditions.push(`name IN (${list})`);
|
|
133
|
+
}
|
|
134
|
+
if (conditions.length)
|
|
135
|
+
sql += ` AND ${conditions.join(' AND ')}`;
|
|
136
|
+
sql += ` ORDER BY file_path, start_line`;
|
|
137
|
+
const rows = this.queryAll(sql);
|
|
138
|
+
return rows.map((r) => this.rowToSymbol(r));
|
|
139
|
+
}
|
|
140
|
+
async listInterfaces(filter) {
|
|
141
|
+
let sql = `SELECT * FROM nodes WHERE kind='interface'`;
|
|
142
|
+
if (filter?.dir)
|
|
143
|
+
sql += ` AND file_path LIKE '${this.escape(filter.dir)}%'`;
|
|
144
|
+
sql += ` ORDER BY file_path, start_line`;
|
|
145
|
+
const rows = this.queryAll(sql);
|
|
146
|
+
return rows
|
|
147
|
+
.filter((r) => !filter?.namePattern || filter.namePattern.test(r.name))
|
|
148
|
+
.map((r) => this.rowToSymbol(r));
|
|
149
|
+
}
|
|
150
|
+
async listImports(file) {
|
|
151
|
+
// CodeGraph stores imports as nodes of kind='import' with edges to the imported module.
|
|
152
|
+
// For v1, we do a simplified query: get all import nodes in this file.
|
|
153
|
+
const rel = file.startsWith(this.sourceDir) ? file.slice(this.sourceDir.length).replace(/^\//, '') : file;
|
|
154
|
+
const sql = `SELECT name, qualified_name FROM nodes WHERE kind='import' AND file_path='${this.escape(rel)}'`;
|
|
155
|
+
const rows = this.queryAll(sql);
|
|
156
|
+
return rows.map((r) => ({
|
|
157
|
+
fromFile: file,
|
|
158
|
+
// qualified_name may encode the module path; name encodes the imported symbol
|
|
159
|
+
toModule: r.qualified_name.includes(':') ? r.qualified_name.split(':')[0] : r.qualified_name,
|
|
160
|
+
symbols: [r.name],
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
async fileSourceText(file) {
|
|
164
|
+
return readFileSync(file, 'utf8');
|
|
165
|
+
}
|
|
166
|
+
async callers(symbol) {
|
|
167
|
+
const sql = `
|
|
168
|
+
SELECT n.* FROM edges e
|
|
169
|
+
JOIN nodes n ON e.from_id = n.id
|
|
170
|
+
WHERE e.to_id = (SELECT id FROM nodes WHERE qualified_name='${this.escape(symbol)}' LIMIT 1)
|
|
171
|
+
AND e.kind='call'
|
|
172
|
+
`;
|
|
173
|
+
const rows = this.queryAll(sql);
|
|
174
|
+
return rows.map((r) => this.rowToSymbol(r));
|
|
175
|
+
}
|
|
176
|
+
async callees(symbol) {
|
|
177
|
+
const sql = `
|
|
178
|
+
SELECT n.* FROM edges e
|
|
179
|
+
JOIN nodes n ON e.to_id = n.id
|
|
180
|
+
WHERE e.from_id = (SELECT id FROM nodes WHERE qualified_name='${this.escape(symbol)}' LIMIT 1)
|
|
181
|
+
AND e.kind='call'
|
|
182
|
+
`;
|
|
183
|
+
const rows = this.queryAll(sql);
|
|
184
|
+
return rows.map((r) => this.rowToSymbol(r));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Per-method fact sheet via SQL (for the call graph) + regex over the body
|
|
188
|
+
* (for everything else: emits, dbWrites, externalCalls, throws, async, branches).
|
|
189
|
+
*
|
|
190
|
+
* The SQL piece is what justifies the CodeGraph backend over grep-only here —
|
|
191
|
+
* tree-sitter resolved cross-file calls deterministically, while a regex over
|
|
192
|
+
* a single method body would only see same-file references.
|
|
193
|
+
*/
|
|
194
|
+
async getMethodDetails(qualifiedName) {
|
|
195
|
+
const methodSql = `
|
|
196
|
+
SELECT * FROM nodes
|
|
197
|
+
WHERE qualified_name='${this.escape(qualifiedName)}'
|
|
198
|
+
AND kind='method'
|
|
199
|
+
LIMIT 1
|
|
200
|
+
`;
|
|
201
|
+
const rows = this.queryAll(methodSql);
|
|
202
|
+
if (rows.length === 0)
|
|
203
|
+
return null;
|
|
204
|
+
const methodRow = rows[0];
|
|
205
|
+
// Resolve absolute file path (consistent with rowToSymbol)
|
|
206
|
+
const filePath = methodRow.file_path.startsWith('/')
|
|
207
|
+
? methodRow.file_path
|
|
208
|
+
: join(this.sourceDir, methodRow.file_path);
|
|
209
|
+
// Read the body — extract via brace-depth scan over the source file
|
|
210
|
+
const fileText = readFileSync(filePath, 'utf8');
|
|
211
|
+
const fileLines = fileText.split('\n');
|
|
212
|
+
const bodyLines = fileLines.slice(methodRow.start_line - 1, methodRow.end_line);
|
|
213
|
+
const body = bodyLines.join('\n');
|
|
214
|
+
// Calls — use the call graph (CodeGraph's strength)
|
|
215
|
+
const calls = await this.callees(qualifiedName);
|
|
216
|
+
return {
|
|
217
|
+
qualifiedName,
|
|
218
|
+
signature: methodRow.signature ?? '',
|
|
219
|
+
body,
|
|
220
|
+
filePath,
|
|
221
|
+
startLine: methodRow.start_line,
|
|
222
|
+
endLine: methodRow.end_line,
|
|
223
|
+
language: methodRow.language,
|
|
224
|
+
calls,
|
|
225
|
+
emits: extractEmits(body),
|
|
226
|
+
dbWrites: extractDbWrites(body),
|
|
227
|
+
externalCalls: extractExternalCalls(body),
|
|
228
|
+
throws: extractThrows(body),
|
|
229
|
+
asyncBoundaries: extractAsyncBoundaries(body),
|
|
230
|
+
branchPoints: countBranchPoints(body),
|
|
231
|
+
bodyTextSliced: sliceBody(body),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// ────────────────────────────────────────────────────────────────────
|
|
235
|
+
// Internals
|
|
236
|
+
// ────────────────────────────────────────────────────────────────────
|
|
237
|
+
/** Run a SQL query; expect 0 or 1 row. Throws if more. */
|
|
238
|
+
queryOne(sql) {
|
|
239
|
+
const rows = this.queryAll(sql);
|
|
240
|
+
if (rows.length === 0)
|
|
241
|
+
return {};
|
|
242
|
+
return rows[0];
|
|
243
|
+
}
|
|
244
|
+
/** Run a SQL query; return all rows as objects (parsed from JSON). */
|
|
245
|
+
queryAll(sql) {
|
|
246
|
+
const stdout = execFileSync(this.sqlite3Path, ['-json', this.dbPath, sql], {
|
|
247
|
+
encoding: 'utf8',
|
|
248
|
+
timeout: 30_000,
|
|
249
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB — large repos can produce big result sets
|
|
250
|
+
});
|
|
251
|
+
if (!stdout.trim())
|
|
252
|
+
return [];
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(stdout);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
throw new Error(`CodeGraph backend: failed to parse sqlite3 -json output: ${err}\n--- stdout ---\n${stdout.slice(0, 500)}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
rowToSymbol(row) {
|
|
261
|
+
let decorators;
|
|
262
|
+
if (row.decorators) {
|
|
263
|
+
try {
|
|
264
|
+
decorators = JSON.parse(row.decorators);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Ignore malformed decorator JSON
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const filePath = row.file_path.startsWith('/')
|
|
271
|
+
? row.file_path
|
|
272
|
+
: join(this.sourceDir, row.file_path);
|
|
273
|
+
return {
|
|
274
|
+
kind: mapKind(row.kind) ?? 'function',
|
|
275
|
+
name: row.name,
|
|
276
|
+
qualifiedName: row.qualified_name,
|
|
277
|
+
filePath,
|
|
278
|
+
startLine: row.start_line,
|
|
279
|
+
endLine: row.end_line,
|
|
280
|
+
signature: row.signature ?? undefined,
|
|
281
|
+
docstring: row.docstring ?? undefined,
|
|
282
|
+
isExported: !!row.is_exported,
|
|
283
|
+
decorators,
|
|
284
|
+
language: row.language,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
escape(s) {
|
|
288
|
+
// Single-quote escaping for SQL string literals.
|
|
289
|
+
return s.replace(/'/g, "''");
|
|
290
|
+
}
|
|
291
|
+
detectBinary(name) {
|
|
292
|
+
try {
|
|
293
|
+
const path = execSync(`which ${name}`, { encoding: 'utf8', timeout: 3000 }).trim();
|
|
294
|
+
if (path)
|
|
295
|
+
return path;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Fall through
|
|
299
|
+
}
|
|
300
|
+
return name; // Hope it's on PATH
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=codegraph.js.map
|