@specverse/engines 6.0.7 → 6.0.9

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.
Files changed (47) hide show
  1. package/dist/analyse-prepass/adapters/index.d.ts +9 -0
  2. package/dist/analyse-prepass/adapters/index.d.ts.map +1 -0
  3. package/dist/analyse-prepass/adapters/index.js +9 -0
  4. package/dist/analyse-prepass/adapters/index.js.map +1 -0
  5. package/dist/analyse-prepass/adapters/typescript-prisma.d.ts +67 -0
  6. package/dist/analyse-prepass/adapters/typescript-prisma.d.ts.map +1 -0
  7. package/dist/analyse-prepass/adapters/typescript-prisma.js +167 -0
  8. package/dist/analyse-prepass/adapters/typescript-prisma.js.map +1 -0
  9. package/dist/analyse-prepass/backends/codegraph.d.ts +56 -0
  10. package/dist/analyse-prepass/backends/codegraph.d.ts.map +1 -0
  11. package/dist/analyse-prepass/backends/codegraph.js +303 -0
  12. package/dist/analyse-prepass/backends/codegraph.js.map +1 -0
  13. package/dist/analyse-prepass/backends/gitnexus.d.ts +71 -0
  14. package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -0
  15. package/dist/analyse-prepass/backends/gitnexus.js +367 -0
  16. package/dist/analyse-prepass/backends/gitnexus.js.map +1 -0
  17. package/dist/analyse-prepass/backends/grep-only.d.ts +33 -0
  18. package/dist/analyse-prepass/backends/grep-only.d.ts.map +1 -0
  19. package/dist/analyse-prepass/backends/grep-only.js +377 -0
  20. package/dist/analyse-prepass/backends/grep-only.js.map +1 -0
  21. package/dist/analyse-prepass/backends/index.d.ts +28 -0
  22. package/dist/analyse-prepass/backends/index.d.ts.map +1 -0
  23. package/dist/analyse-prepass/backends/index.js +36 -0
  24. package/dist/analyse-prepass/backends/index.js.map +1 -0
  25. package/dist/analyse-prepass/backends/method-patterns.d.ts +27 -0
  26. package/dist/analyse-prepass/backends/method-patterns.d.ts.map +1 -0
  27. package/dist/analyse-prepass/backends/method-patterns.js +177 -0
  28. package/dist/analyse-prepass/backends/method-patterns.js.map +1 -0
  29. package/dist/analyse-prepass/backends/walk.d.ts +7 -0
  30. package/dist/analyse-prepass/backends/walk.d.ts.map +1 -0
  31. package/dist/analyse-prepass/backends/walk.js +105 -0
  32. package/dist/analyse-prepass/backends/walk.js.map +1 -0
  33. package/dist/analyse-prepass/index.d.ts +76 -0
  34. package/dist/analyse-prepass/index.d.ts.map +1 -0
  35. package/dist/analyse-prepass/index.js +114 -0
  36. package/dist/analyse-prepass/index.js.map +1 -0
  37. package/dist/analyse-prepass/interface.d.ts +222 -0
  38. package/dist/analyse-prepass/interface.d.ts.map +1 -0
  39. package/dist/analyse-prepass/interface.js +20 -0
  40. package/dist/analyse-prepass/interface.js.map +1 -0
  41. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +104 -0
  42. package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +25 -4
  43. package/libs/instance-factories/cli/templates/commander/command-generator.ts +104 -0
  44. package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +43 -5
  45. package/package.json +5 -1
  46. package/libs/instance-factories/cli/templates/commander/command-generator.d.ts +0 -14
  47. package/libs/instance-factories/cli/templates/commander/command-generator.js +0 -182
@@ -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