@sigil-engine/core 0.1.0

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 (48) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/dist/cache.d.ts +24 -0
  3. package/dist/cache.d.ts.map +1 -0
  4. package/dist/cache.js +97 -0
  5. package/dist/cache.js.map +1 -0
  6. package/dist/diff.d.ts +34 -0
  7. package/dist/diff.d.ts.map +1 -0
  8. package/dist/diff.js +319 -0
  9. package/dist/diff.js.map +1 -0
  10. package/dist/extract.d.ts +27 -0
  11. package/dist/extract.d.ts.map +1 -0
  12. package/dist/extract.js +162 -0
  13. package/dist/extract.js.map +1 -0
  14. package/dist/index.d.ts +26 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +21 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/plugin.d.ts +103 -0
  19. package/dist/plugin.d.ts.map +1 -0
  20. package/dist/plugin.js +47 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/dist/safety.d.ts +25 -0
  23. package/dist/safety.d.ts.map +1 -0
  24. package/dist/safety.js +139 -0
  25. package/dist/safety.js.map +1 -0
  26. package/dist/schema.d.ts +184 -0
  27. package/dist/schema.d.ts.map +1 -0
  28. package/dist/schema.js +33 -0
  29. package/dist/schema.js.map +1 -0
  30. package/dist/serialize.d.ts +16 -0
  31. package/dist/serialize.d.ts.map +1 -0
  32. package/dist/serialize.js +75 -0
  33. package/dist/serialize.js.map +1 -0
  34. package/package.json +35 -0
  35. package/src/cache.ts +133 -0
  36. package/src/diff.ts +421 -0
  37. package/src/extract.ts +196 -0
  38. package/src/index.ts +94 -0
  39. package/src/plugin.ts +186 -0
  40. package/src/safety.ts +185 -0
  41. package/src/schema.ts +270 -0
  42. package/src/serialize.ts +97 -0
  43. package/tests/cache.test.ts +47 -0
  44. package/tests/diff.test.ts +222 -0
  45. package/tests/plugin.test.ts +107 -0
  46. package/tests/schema.test.ts +132 -0
  47. package/tests/serialize.test.ts +92 -0
  48. package/tsconfig.json +20 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Sigil Plugin System — the extensibility contract.
3
+ *
4
+ * Every language parser implements `ExtractorPlugin`. The core engine
5
+ * discovers registered plugins and orchestrates them.
6
+ *
7
+ * To add Python support:
8
+ * 1. Create @sigil-engine/parser-python
9
+ * 2. Implement ExtractorPlugin
10
+ * 3. Export a default plugin instance
11
+ * 4. Users install it — Sigil discovers it automatically
12
+ */
13
+
14
+ import type { ContextDocument } from "./schema.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Plugin interface
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * The parsed project representation passed to plugins.
22
+ * Language-agnostic — each plugin casts or wraps this with its own type.
23
+ */
24
+ export interface ParsedProject {
25
+ /** Absolute path to the project root */
26
+ projectPath: string;
27
+ /** List of source file paths (absolute) */
28
+ sourceFiles: string[];
29
+ /** Package manifest (package.json, pyproject.toml, etc.) parsed as object */
30
+ manifest?: Record<string, unknown>;
31
+ }
32
+
33
+ /**
34
+ * Result returned by a plugin's extract method.
35
+ * Partial<ContextDocument> — plugins only fill what they understand.
36
+ */
37
+ export type ExtractResult = Partial<Omit<ContextDocument, "version" | "project" | "_extraction">>;
38
+
39
+ /**
40
+ * The core plugin contract. Every language parser implements this.
41
+ */
42
+ export interface ExtractorPlugin {
43
+ /** Unique plugin name (e.g., "typescript", "python", "go") */
44
+ name: string;
45
+
46
+ /** Semver version of the plugin */
47
+ version: string;
48
+
49
+ /** Languages/frameworks this plugin handles */
50
+ languages: string[];
51
+
52
+ /**
53
+ * Check if this plugin should run for the given project.
54
+ * Called before extract() — return false to skip.
55
+ *
56
+ * Example: typescript plugin checks for tsconfig.json or .ts files
57
+ */
58
+ detect(projectPath: string): boolean | Promise<boolean>;
59
+
60
+ /**
61
+ * Initialize the plugin for a specific project.
62
+ * Returns a ParsedProject (or plugin-specific extension of it).
63
+ * Called once before extract().
64
+ */
65
+ init(projectPath: string): ParsedProject | Promise<ParsedProject>;
66
+
67
+ /**
68
+ * Extract context from the project.
69
+ * Returns partial context — the orchestrator merges results from all plugins.
70
+ */
71
+ extract(project: ParsedProject): ExtractResult | Promise<ExtractResult>;
72
+
73
+ /**
74
+ * Optional: handle apply intents for elements this plugin created.
75
+ * Called by the apply engine when an intent targets a _meta.tags
76
+ * value owned by this plugin.
77
+ */
78
+ apply?(intent: ApplyIntent, projectPath: string): ApplyResult | Promise<ApplyResult>;
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Apply types (used by plugins that support apply)
83
+ // ---------------------------------------------------------------------------
84
+
85
+ export type IntentType =
86
+ | "AddEntity"
87
+ | "RemoveEntity"
88
+ | "AddField"
89
+ | "RemoveField"
90
+ | "ModifyField"
91
+ | "RenameSymbol"
92
+ | "AddComponent"
93
+ | "RemoveComponent"
94
+ | "AddRoute"
95
+ | "RemoveRoute"
96
+ | "AddMethod"
97
+ | "RemoveMethod"
98
+ | "ModifyProps"
99
+ | "ChangeSignature"
100
+ | "MoveFile"
101
+ | "AddDependency"
102
+ | "RemoveDependency";
103
+
104
+ export type IntentConfidence = "deterministic" | "needs-ai";
105
+
106
+ export interface ApplyIntent {
107
+ type: IntentType;
108
+ confidence: IntentConfidence;
109
+ target_file?: string;
110
+ target_symbol?: string;
111
+ description: string;
112
+ old_value?: unknown;
113
+ new_value?: unknown;
114
+ priority: number;
115
+ }
116
+
117
+ export interface ApplyResult {
118
+ intent: ApplyIntent;
119
+ success: boolean;
120
+ error?: string;
121
+ files_created: string[];
122
+ files_modified: string[];
123
+ preview?: string;
124
+ code?: string;
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Plugin registry
129
+ // ---------------------------------------------------------------------------
130
+
131
+ const registry: ExtractorPlugin[] = [];
132
+
133
+ /** Register a plugin with Sigil */
134
+ export function registerPlugin(plugin: ExtractorPlugin): void {
135
+ // Prevent duplicate registration
136
+ const existing = registry.findIndex((p) => p.name === plugin.name);
137
+ if (existing >= 0) {
138
+ registry[existing] = plugin;
139
+ } else {
140
+ registry.push(plugin);
141
+ }
142
+ }
143
+
144
+ /** Get all registered plugins */
145
+ export function getPlugins(): readonly ExtractorPlugin[] {
146
+ return registry;
147
+ }
148
+
149
+ /** Get plugins that detect support for a given project */
150
+ export async function detectPlugins(projectPath: string): Promise<ExtractorPlugin[]> {
151
+ const detected: ExtractorPlugin[] = [];
152
+ for (const plugin of registry) {
153
+ const supports = await plugin.detect(projectPath);
154
+ if (supports) {
155
+ detected.push(plugin);
156
+ }
157
+ }
158
+ return detected;
159
+ }
160
+
161
+ /** Clear all registered plugins (for testing) */
162
+ export function clearPlugins(): void {
163
+ registry.length = 0;
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Progress reporting
168
+ // ---------------------------------------------------------------------------
169
+
170
+ export type ProgressPhase =
171
+ | "init"
172
+ | "detect"
173
+ | "extract"
174
+ | "merge"
175
+ | "serialize"
176
+ | "cache"
177
+ | "done";
178
+
179
+ export interface ProgressEvent {
180
+ phase: ProgressPhase;
181
+ plugin?: string;
182
+ message: string;
183
+ elapsed_ms?: number;
184
+ }
185
+
186
+ export type ProgressCallback = (event: ProgressEvent) => void;
package/src/safety.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Apply safety mechanisms — git stash before, validation after, rollback on failure.
3
+ *
4
+ * SECURITY: All git operations use execFileSync (not execSync) to prevent
5
+ * shell injection. Destructive operations require explicit confirmation.
6
+ */
7
+
8
+ import { execFileSync } from "child_process";
9
+ import { resolve } from "path";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Pre-apply: save state
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface SafetyCheckpoint {
16
+ hadDirtyFiles: boolean;
17
+ stashCreated: boolean;
18
+ originalHead: string;
19
+ }
20
+
21
+ /**
22
+ * Callback for confirming destructive operations.
23
+ * Return true to proceed, false to abort.
24
+ */
25
+ export type ConfirmCallback = (message: string) => boolean | Promise<boolean>;
26
+
27
+ /** Default confirmation — always denies (safe default) */
28
+ const denyAll: ConfirmCallback = () => false;
29
+
30
+ export function preApplySafety(
31
+ projectPath: string,
32
+ confirm: ConfirmCallback = denyAll,
33
+ ): SafetyCheckpoint {
34
+ const absPath = resolve(projectPath);
35
+
36
+ // Get current HEAD — using execFileSync (no shell)
37
+ let originalHead = "";
38
+ try {
39
+ originalHead = execFileSync("git", ["rev-parse", "HEAD"], {
40
+ cwd: absPath,
41
+ encoding: "utf-8",
42
+ stdio: ["pipe", "pipe", "pipe"],
43
+ }).trim();
44
+ } catch {
45
+ // Not a git repo — skip safety
46
+ return { hadDirtyFiles: false, stashCreated: false, originalHead: "" };
47
+ }
48
+
49
+ // Check for dirty files
50
+ let gitStatus = "";
51
+ try {
52
+ gitStatus = execFileSync("git", ["status", "--porcelain"], {
53
+ cwd: absPath,
54
+ encoding: "utf-8",
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ }).trim();
57
+ } catch {
58
+ return { hadDirtyFiles: false, stashCreated: false, originalHead };
59
+ }
60
+
61
+ const hadDirtyFiles = gitStatus.length > 0;
62
+
63
+ if (hadDirtyFiles) {
64
+ try {
65
+ execFileSync("git", ["stash", "push", "-m", "sigil-pre-apply"], {
66
+ cwd: absPath,
67
+ encoding: "utf-8",
68
+ stdio: ["pipe", "pipe", "pipe"],
69
+ });
70
+ return { hadDirtyFiles, stashCreated: true, originalHead };
71
+ } catch {
72
+ return { hadDirtyFiles, stashCreated: false, originalHead };
73
+ }
74
+ }
75
+
76
+ return { hadDirtyFiles, stashCreated: false, originalHead };
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Post-apply: validate types
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export interface ValidationResult {
84
+ typesOk: boolean;
85
+ errors: string[];
86
+ }
87
+
88
+ export function postApplyValidation(projectPath: string): ValidationResult {
89
+ const absPath = resolve(projectPath);
90
+
91
+ try {
92
+ execFileSync("npx", ["tsc", "--noEmit"], {
93
+ cwd: absPath,
94
+ encoding: "utf-8",
95
+ stdio: ["pipe", "pipe", "pipe"],
96
+ timeout: 30000,
97
+ });
98
+ return { typesOk: true, errors: [] };
99
+ } catch (err: unknown) {
100
+ const output =
101
+ (err as { stdout?: string }).stdout ??
102
+ (err as { stderr?: string }).stderr ??
103
+ "";
104
+ const errors = output
105
+ .split("\n")
106
+ .filter((line) => line.includes("error TS"))
107
+ .slice(0, 10);
108
+ return { typesOk: false, errors };
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Rollback
114
+ // ---------------------------------------------------------------------------
115
+
116
+ export async function rollbackApply(
117
+ projectPath: string,
118
+ checkpoint: SafetyCheckpoint,
119
+ confirm: ConfirmCallback = denyAll,
120
+ ): Promise<boolean> {
121
+ const absPath = resolve(projectPath);
122
+
123
+ // Require confirmation before destructive rollback
124
+ const confirmed = await confirm(
125
+ "Apply produced type errors. Roll back all changes and restore previous state?",
126
+ );
127
+
128
+ if (!confirmed) {
129
+ return false;
130
+ }
131
+
132
+ // Discard apply changes — execFileSync, no shell
133
+ try {
134
+ execFileSync("git", ["checkout", "."], {
135
+ cwd: absPath,
136
+ encoding: "utf-8",
137
+ stdio: ["pipe", "pipe", "pipe"],
138
+ });
139
+ } catch {
140
+ // Could not roll back
141
+ return false;
142
+ }
143
+
144
+ // Clean untracked files created by apply
145
+ try {
146
+ execFileSync("git", ["clean", "-fd"], {
147
+ cwd: absPath,
148
+ encoding: "utf-8",
149
+ stdio: ["pipe", "pipe", "pipe"],
150
+ });
151
+ } catch {
152
+ // Non-fatal
153
+ }
154
+
155
+ // Restore stash if we created one
156
+ if (checkpoint.stashCreated) {
157
+ try {
158
+ execFileSync("git", ["stash", "pop"], {
159
+ cwd: absPath,
160
+ encoding: "utf-8",
161
+ stdio: ["pipe", "pipe", "pipe"],
162
+ });
163
+ } catch {
164
+ // Stash pop failed — user must handle manually
165
+ }
166
+ }
167
+
168
+ return true;
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Git commit helper (for post-apply)
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export function getGitStatus(projectPath: string): string {
176
+ try {
177
+ return execFileSync("git", ["status", "--porcelain"], {
178
+ cwd: resolve(projectPath),
179
+ encoding: "utf-8",
180
+ stdio: ["pipe", "pipe", "pipe"],
181
+ }).trim();
182
+ } catch {
183
+ return "";
184
+ }
185
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Sigil Context Document Schema — the universal intermediate representation.
3
+ *
4
+ * DESIGN PRINCIPLE: This schema is LANGUAGE-AGNOSTIC. It contains zero
5
+ * references to any specific language, framework, ORM, or toolchain.
6
+ * Technology-specific metadata is contributed by plugins via the `tags`
7
+ * system and extensible record types.
8
+ *
9
+ * The _meta anchor on every element enables bidirectional sync —
10
+ * each context element links back to its source location.
11
+ */
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Meta anchor — the bidirectional link
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface Meta {
18
+ /** Relative path from project root to source file */
19
+ file: string;
20
+ /** Exported symbol name (function, class, variable, type) */
21
+ symbol: string;
22
+ /** Generic element category — language-agnostic */
23
+ type: MetaType;
24
+ /** Plugin-contributed tags for technology-specific classification */
25
+ tags?: string[];
26
+ /** Line number in source file (for precise anchoring) */
27
+ line?: number;
28
+ /** Truncated SHA-256 hash of source file at extraction time */
29
+ hash?: string;
30
+ }
31
+
32
+ /**
33
+ * Generic element categories. These are structural roles, not
34
+ * technology-specific types. Plugins use `tags` for specificity.
35
+ *
36
+ * Example: a Drizzle table has type "entity" with tags ["orm:drizzle", "db:postgresql"]
37
+ * Example: a React component has type "component" with tags ["framework:react", "client"]
38
+ * Example: a Django model has type "entity" with tags ["orm:django", "db:postgresql"]
39
+ */
40
+ export type MetaType =
41
+ | "entity" // Data model (DB table, ORM model, GraphQL type)
42
+ | "component" // UI component (React, Vue, Svelte, etc.)
43
+ | "route" // HTTP route / API endpoint
44
+ | "function" // Standalone function
45
+ | "class" // Class declaration
46
+ | "type" // Type alias, interface, enum
47
+ | "constant" // Constant / config value
48
+ | "middleware" // Request middleware / interceptor
49
+ | "hook" // Framework-specific hook (React hook, lifecycle, etc.)
50
+ | "config"; // Configuration file / entry
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Stack & project metadata (extensible)
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Project stack information — plugins contribute their own key-value pairs.
58
+ *
59
+ * Common keys (by convention, not enforced):
60
+ * framework, language, orm, database, ui, auth, testing, package_manager
61
+ *
62
+ * Values are strings or string arrays.
63
+ */
64
+ export type StackInfo = Record<string, string | string[]>;
65
+
66
+ /** Extraction metadata — when and how the context was generated */
67
+ export interface ExtractionMeta {
68
+ extracted_at: string;
69
+ sigil_version?: string;
70
+ source_commit?: string;
71
+ extraction_ms?: number;
72
+ plugins_used?: string[];
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Entities (data models — ORM tables, GraphQL types, etc.)
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export interface Entity {
80
+ name: string;
81
+ _meta: Meta;
82
+ fields: EntityField[];
83
+ relations: EntityRelation[];
84
+ }
85
+
86
+ export interface EntityField {
87
+ name: string;
88
+ type: string;
89
+ primary?: boolean;
90
+ unique?: boolean;
91
+ nullable?: boolean;
92
+ default?: string;
93
+ references?: { table: string; column: string };
94
+ }
95
+
96
+ export interface EntityRelation {
97
+ to: string;
98
+ type: "one-to-one" | "one-to-many" | "many-to-many";
99
+ via?: string;
100
+ through?: string;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Components (UI elements — framework-agnostic)
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export interface Component {
108
+ name: string;
109
+ _meta: Meta;
110
+ props: ComponentProp[];
111
+ dependencies: string[];
112
+ hooks: string[];
113
+ data_sources: string[];
114
+ route?: string;
115
+ /** Plugin-specific flags via tags instead of hardcoded booleans */
116
+ }
117
+
118
+ export interface ComponentProp {
119
+ name: string;
120
+ type: string;
121
+ required?: boolean;
122
+ default?: string;
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Routes (API endpoints — framework-agnostic)
127
+ // ---------------------------------------------------------------------------
128
+
129
+ export interface Route {
130
+ path: string;
131
+ _meta: Meta;
132
+ methods: Record<string, RouteMethod>;
133
+ }
134
+
135
+ export interface RouteMethod {
136
+ auth?: boolean;
137
+ returns?: string;
138
+ params?: string[];
139
+ body?: string;
140
+ description?: string;
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Exports (generic symbols not captured elsewhere)
145
+ // ---------------------------------------------------------------------------
146
+
147
+ export interface ExportedSymbol {
148
+ name: string;
149
+ _meta: Meta;
150
+ kind: "function" | "class" | "interface" | "type" | "enum" | "constant";
151
+ signature?: string;
152
+ description?: string;
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Patterns & conventions (extensible key-value)
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * Detected architectural patterns. Plugins contribute entries.
161
+ * Keys are pattern categories (e.g., "auth", "data_fetching", "state").
162
+ * Values describe the detected pattern.
163
+ */
164
+ export type Patterns = Record<string, string>;
165
+
166
+ /**
167
+ * Detected conventions. Plugins contribute entries.
168
+ * Keys are convention categories (e.g., "file_naming", "import_style").
169
+ * Values describe the convention.
170
+ */
171
+ export type Conventions = Record<string, string>;
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Dependencies (from package manifest)
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export interface Dependency {
178
+ name: string;
179
+ version: string;
180
+ dev?: boolean;
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Call graph (behavior layer)
185
+ // ---------------------------------------------------------------------------
186
+
187
+ export interface CallEdge {
188
+ /** Fully qualified caller: "file::symbol" */
189
+ caller: string;
190
+ /** Fully qualified callee: "file::symbol" */
191
+ callee: string;
192
+ location: { file: string; line: number };
193
+ data_flow?: { args: string[]; returns: string };
194
+ }
195
+
196
+ export interface ComponentEdge {
197
+ source: string;
198
+ target: string;
199
+ weight: number;
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Environment variables
204
+ // ---------------------------------------------------------------------------
205
+
206
+ export interface EnvVar {
207
+ name: string;
208
+ required: boolean;
209
+ used_in: string[];
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // The root document
214
+ // ---------------------------------------------------------------------------
215
+
216
+ export interface ContextDocument {
217
+ /** Schema version for compatibility checking */
218
+ version: string;
219
+ /** Project name */
220
+ project: string;
221
+ /** Extraction metadata */
222
+ _extraction: ExtractionMeta;
223
+ /** Technology stack (extensible key-value) */
224
+ stack: StackInfo;
225
+ /** Data entities (DB tables, models) */
226
+ entities: Entity[];
227
+ /** UI components */
228
+ components: Component[];
229
+ /** API routes / endpoints */
230
+ routes: Route[];
231
+ /** Other exported symbols */
232
+ exports: ExportedSymbol[];
233
+ /** Function-level call graph */
234
+ call_graph: CallEdge[];
235
+ /** File-level interaction graph */
236
+ component_graph: ComponentEdge[];
237
+ /** Environment variables */
238
+ env_vars: EnvVar[];
239
+ /** Detected architectural patterns */
240
+ patterns: Patterns;
241
+ /** Detected conventions */
242
+ conventions: Conventions;
243
+ /** Package dependencies */
244
+ dependencies: Dependency[];
245
+ /** Plugin-contributed custom sections */
246
+ extensions?: Record<string, unknown>;
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Factory
251
+ // ---------------------------------------------------------------------------
252
+
253
+ export function createEmptyDocument(project: string): ContextDocument {
254
+ return {
255
+ version: "1.0",
256
+ project,
257
+ _extraction: { extracted_at: new Date().toISOString() },
258
+ stack: {},
259
+ entities: [],
260
+ components: [],
261
+ routes: [],
262
+ exports: [],
263
+ call_graph: [],
264
+ component_graph: [],
265
+ env_vars: [],
266
+ patterns: {},
267
+ conventions: {},
268
+ dependencies: [],
269
+ };
270
+ }