@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/diff.ts ADDED
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Context diff engine — structural comparison of two context documents.
3
+ *
4
+ * Compares entities, components, routes, exports, patterns, conventions,
5
+ * stack, and dependencies. Outputs a structured DiffResult.
6
+ *
7
+ * PURE: No I/O. Takes two documents, returns a diff.
8
+ */
9
+
10
+ import type {
11
+ ContextDocument,
12
+ Entity,
13
+ Component,
14
+ Route,
15
+ ExportedSymbol,
16
+ } from "./schema.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Diff result types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export type ChangeType = "added" | "removed" | "modified" | "renamed";
23
+
24
+ export type ChangeCategory =
25
+ | "entity"
26
+ | "component"
27
+ | "route"
28
+ | "export"
29
+ | "pattern"
30
+ | "convention"
31
+ | "stack"
32
+ | "dependency";
33
+
34
+ export interface Change<T = unknown> {
35
+ type: ChangeType;
36
+ name: string;
37
+ category: ChangeCategory;
38
+ old?: T;
39
+ new?: T;
40
+ details?: string[];
41
+ }
42
+
43
+ export interface DiffResult {
44
+ summary: {
45
+ total_changes: number;
46
+ added: number;
47
+ removed: number;
48
+ modified: number;
49
+ };
50
+ changes: Change[];
51
+ stack_changes: Change[];
52
+ pattern_changes: Change[];
53
+ convention_changes: Change[];
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Main diff
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export function diffContexts(oldDoc: ContextDocument, newDoc: ContextDocument): DiffResult {
61
+ const changes: Change[] = [];
62
+
63
+ changes.push(...diffNamedList(oldDoc.entities, newDoc.entities, "entity", diffEntity));
64
+ changes.push(...diffNamedList(oldDoc.components, newDoc.components, "component", diffComponent));
65
+ changes.push(...diffByKey(oldDoc.routes, newDoc.routes, "route", (r) => r.path, diffRoute));
66
+ changes.push(...diffNamedList(oldDoc.exports, newDoc.exports, "export", diffExport));
67
+ changes.push(...diffDependencies(oldDoc, newDoc));
68
+
69
+ const stack_changes = diffRecord(oldDoc.stack ?? {}, newDoc.stack ?? {}, "stack");
70
+ const pattern_changes = diffRecord(
71
+ oldDoc.patterns as Record<string, unknown>,
72
+ newDoc.patterns as Record<string, unknown>,
73
+ "pattern",
74
+ );
75
+ const convention_changes = diffRecord(
76
+ oldDoc.conventions as Record<string, unknown>,
77
+ newDoc.conventions as Record<string, unknown>,
78
+ "convention",
79
+ );
80
+
81
+ const allChanges = [...changes, ...stack_changes, ...pattern_changes, ...convention_changes];
82
+
83
+ return {
84
+ summary: {
85
+ total_changes: allChanges.length,
86
+ added: allChanges.filter((c) => c.type === "added").length,
87
+ removed: allChanges.filter((c) => c.type === "removed").length,
88
+ modified: allChanges.filter((c) => c.type === "modified").length,
89
+ },
90
+ changes,
91
+ stack_changes,
92
+ pattern_changes,
93
+ convention_changes,
94
+ };
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Named list diffing (entities, components, exports)
99
+ // ---------------------------------------------------------------------------
100
+
101
+ interface Named {
102
+ name: string;
103
+ _meta?: { file: string; symbol?: string };
104
+ }
105
+
106
+ function diffNamedList<T extends Named>(
107
+ oldList: T[],
108
+ newList: T[],
109
+ category: ChangeCategory,
110
+ detailDiff: (old: T, neu: T) => string[],
111
+ ): Change[] {
112
+ const changes: Change[] = [];
113
+
114
+ const oldMap = new Map<string, T>();
115
+ for (const item of oldList) {
116
+ const key = item._meta ? `${item._meta.file}::${item.name}` : item.name;
117
+ oldMap.set(key, item);
118
+ }
119
+
120
+ const newMap = new Map<string, T>();
121
+ for (const item of newList) {
122
+ const key = item._meta ? `${item._meta.file}::${item.name}` : item.name;
123
+ newMap.set(key, item);
124
+ }
125
+
126
+ for (const [key, newItem] of newMap) {
127
+ const oldItem = oldMap.get(key);
128
+ if (!oldItem) {
129
+ const byName = oldList.find((o) => o.name === newItem.name);
130
+ if (byName) {
131
+ const details = detailDiff(byName, newItem);
132
+ if (details.length > 0 || byName._meta?.file !== newItem._meta?.file) {
133
+ const allDetails = byName._meta?.file !== newItem._meta?.file
134
+ ? [`moved: ${byName._meta?.file} → ${newItem._meta?.file}`, ...details]
135
+ : details;
136
+ changes.push({ type: "modified", name: newItem.name, category, old: byName, new: newItem, details: allDetails });
137
+ }
138
+ } else {
139
+ changes.push({ type: "added", name: newItem.name, category, new: newItem });
140
+ }
141
+ } else {
142
+ const details = detailDiff(oldItem, newItem);
143
+ if (details.length > 0) {
144
+ changes.push({ type: "modified", name: newItem.name, category, old: oldItem, new: newItem, details });
145
+ }
146
+ }
147
+ }
148
+
149
+ for (const [key, oldItem] of oldMap) {
150
+ if (!newMap.has(key)) {
151
+ const byName = newList.find((n) => n.name === oldItem.name);
152
+ if (!byName) {
153
+ changes.push({ type: "removed", name: oldItem.name, category, old: oldItem });
154
+ }
155
+ }
156
+ }
157
+
158
+ return changes;
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Key-based diffing (routes)
163
+ // ---------------------------------------------------------------------------
164
+
165
+ function diffByKey<T>(
166
+ oldList: T[],
167
+ newList: T[],
168
+ category: ChangeCategory,
169
+ keyFn: (item: T) => string,
170
+ detailDiff: (old: T, neu: T) => string[],
171
+ ): Change[] {
172
+ const changes: Change[] = [];
173
+ const oldMap = new Map(oldList.map((item) => [keyFn(item), item]));
174
+ const newMap = new Map(newList.map((item) => [keyFn(item), item]));
175
+
176
+ for (const [key, newItem] of newMap) {
177
+ const oldItem = oldMap.get(key);
178
+ if (!oldItem) {
179
+ changes.push({ type: "added", name: key, category, new: newItem });
180
+ } else {
181
+ const details = detailDiff(oldItem, newItem);
182
+ if (details.length > 0) {
183
+ changes.push({ type: "modified", name: key, category, old: oldItem, new: newItem, details });
184
+ }
185
+ }
186
+ }
187
+
188
+ for (const [key, oldItem] of oldMap) {
189
+ if (!newMap.has(key)) {
190
+ changes.push({ type: "removed", name: key, category, old: oldItem });
191
+ }
192
+ }
193
+
194
+ return changes;
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Record diffing (stack, patterns, conventions)
199
+ // ---------------------------------------------------------------------------
200
+
201
+ function diffRecord(
202
+ oldObj: Record<string, unknown>,
203
+ newObj: Record<string, unknown>,
204
+ category: ChangeCategory,
205
+ ): Change[] {
206
+ const changes: Change[] = [];
207
+ const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
208
+
209
+ for (const key of allKeys) {
210
+ const oldVal = oldObj[key];
211
+ const newVal = newObj[key];
212
+
213
+ if (oldVal === undefined && newVal !== undefined) {
214
+ changes.push({ type: "added", name: key, category, new: newVal });
215
+ } else if (oldVal !== undefined && newVal === undefined) {
216
+ changes.push({ type: "removed", name: key, category, old: oldVal });
217
+ } else if (!deepEqual(oldVal, newVal)) {
218
+ changes.push({
219
+ type: "modified",
220
+ name: key,
221
+ category,
222
+ old: oldVal,
223
+ new: newVal,
224
+ details: [`${JSON.stringify(oldVal)} → ${JSON.stringify(newVal)}`],
225
+ });
226
+ }
227
+ }
228
+
229
+ return changes;
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Dependency diffing
234
+ // ---------------------------------------------------------------------------
235
+
236
+ function diffDependencies(oldDoc: ContextDocument, newDoc: ContextDocument): Change[] {
237
+ const changes: Change[] = [];
238
+ const oldDeps = new Map((oldDoc.dependencies ?? []).map((d) => [d.name, d.version]));
239
+ const newDeps = new Map((newDoc.dependencies ?? []).map((d) => [d.name, d.version]));
240
+
241
+ for (const [name, version] of newDeps) {
242
+ if (!oldDeps.has(name)) {
243
+ changes.push({ type: "added", name, category: "dependency", new: version });
244
+ } else if (oldDeps.get(name) !== version) {
245
+ changes.push({
246
+ type: "modified", name, category: "dependency",
247
+ old: oldDeps.get(name), new: version,
248
+ details: [`${oldDeps.get(name)} → ${version}`],
249
+ });
250
+ }
251
+ }
252
+ for (const [name] of oldDeps) {
253
+ if (!newDeps.has(name)) {
254
+ changes.push({ type: "removed", name, category: "dependency", old: oldDeps.get(name) });
255
+ }
256
+ }
257
+
258
+ return changes;
259
+ }
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // Detail diff functions
263
+ // ---------------------------------------------------------------------------
264
+
265
+ function diffEntity(old: Entity, neu: Entity): string[] {
266
+ const details: string[] = [];
267
+ const oldFields = new Map(old.fields.map((f) => [f.name, f]));
268
+ const newFields = new Map(neu.fields.map((f) => [f.name, f]));
269
+
270
+ for (const [name, newField] of newFields) {
271
+ if (!oldFields.has(name)) {
272
+ details.push(`+ field: ${name} (${newField.type})`);
273
+ } else {
274
+ const oldField = oldFields.get(name)!;
275
+ if (oldField.type !== newField.type) details.push(`~ field ${name}: type ${oldField.type} → ${newField.type}`);
276
+ if (oldField.nullable !== newField.nullable) details.push(`~ field ${name}: nullable ${oldField.nullable} → ${newField.nullable}`);
277
+ if (oldField.unique !== newField.unique) details.push(`~ field ${name}: unique ${oldField.unique} → ${newField.unique}`);
278
+ }
279
+ }
280
+ for (const name of oldFields.keys()) {
281
+ if (!newFields.has(name)) details.push(`- field: ${name}`);
282
+ }
283
+
284
+ const oldRels = new Set(old.relations.map((r) => `${r.to}:${r.type}`));
285
+ const newRels = new Set(neu.relations.map((r) => `${r.to}:${r.type}`));
286
+ for (const rel of newRels) if (!oldRels.has(rel)) details.push(`+ relation: ${rel}`);
287
+ for (const rel of oldRels) if (!newRels.has(rel)) details.push(`- relation: ${rel}`);
288
+
289
+ return details;
290
+ }
291
+
292
+ function diffComponent(old: Component, neu: Component): string[] {
293
+ const details: string[] = [];
294
+
295
+ const oldProps = new Map(old.props.map((p) => [p.name, p]));
296
+ const newProps = new Map(neu.props.map((p) => [p.name, p]));
297
+ for (const [name, newProp] of newProps) {
298
+ if (!oldProps.has(name)) {
299
+ details.push(`+ prop: ${name}: ${newProp.type}`);
300
+ } else if (oldProps.get(name)!.type !== newProp.type) {
301
+ details.push(`~ prop ${name}: ${oldProps.get(name)!.type} → ${newProp.type}`);
302
+ }
303
+ }
304
+ for (const name of oldProps.keys()) {
305
+ if (!newProps.has(name)) details.push(`- prop: ${name}`);
306
+ }
307
+
308
+ const addedDeps = neu.dependencies.filter((d) => !old.dependencies.includes(d));
309
+ const removedDeps = old.dependencies.filter((d) => !neu.dependencies.includes(d));
310
+ for (const d of addedDeps) details.push(`+ dep: ${d}`);
311
+ for (const d of removedDeps) details.push(`- dep: ${d}`);
312
+
313
+ return details;
314
+ }
315
+
316
+ function diffRoute(old: Route, neu: Route): string[] {
317
+ const details: string[] = [];
318
+ const allMethods = new Set([...Object.keys(old.methods), ...Object.keys(neu.methods)]);
319
+
320
+ for (const method of allMethods) {
321
+ if (!old.methods[method] && neu.methods[method]) {
322
+ details.push(`+ method: ${method}`);
323
+ } else if (old.methods[method] && !neu.methods[method]) {
324
+ details.push(`- method: ${method}`);
325
+ } else if (old.methods[method] && neu.methods[method]) {
326
+ if (old.methods[method].auth !== neu.methods[method].auth) {
327
+ details.push(`~ ${method} auth: ${old.methods[method].auth} → ${neu.methods[method].auth}`);
328
+ }
329
+ }
330
+ }
331
+
332
+ return details;
333
+ }
334
+
335
+ function diffExport(old: ExportedSymbol, neu: ExportedSymbol): string[] {
336
+ const details: string[] = [];
337
+ if (old.kind !== neu.kind) details.push(`~ kind: ${old.kind} → ${neu.kind}`);
338
+ if (old.signature !== neu.signature) details.push(`~ signature changed`);
339
+ return details;
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Deep equality (replaces JSON.stringify comparison)
344
+ // ---------------------------------------------------------------------------
345
+
346
+ function deepEqual(a: unknown, b: unknown): boolean {
347
+ if (a === b) return true;
348
+ if (a === null || b === null) return false;
349
+ if (typeof a !== typeof b) return false;
350
+
351
+ if (Array.isArray(a) && Array.isArray(b)) {
352
+ if (a.length !== b.length) return false;
353
+ return a.every((val, i) => deepEqual(val, b[i]));
354
+ }
355
+
356
+ if (typeof a === "object" && typeof b === "object") {
357
+ const aObj = a as Record<string, unknown>;
358
+ const bObj = b as Record<string, unknown>;
359
+ const aKeys = Object.keys(aObj);
360
+ const bKeys = Object.keys(bObj);
361
+ if (aKeys.length !== bKeys.length) return false;
362
+ return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
363
+ }
364
+
365
+ return false;
366
+ }
367
+
368
+ // ---------------------------------------------------------------------------
369
+ // Format diff for terminal output
370
+ // ---------------------------------------------------------------------------
371
+
372
+ export function formatDiff(result: DiffResult): string {
373
+ const lines: string[] = [];
374
+
375
+ lines.push(`\n Context Diff Summary`);
376
+ lines.push(` ${"─".repeat(20)}`);
377
+ lines.push(` ${result.summary.total_changes} changes: +${result.summary.added} added, -${result.summary.removed} removed, ~${result.summary.modified} modified\n`);
378
+
379
+ const allChanges = [
380
+ ...result.stack_changes,
381
+ ...result.pattern_changes,
382
+ ...result.convention_changes,
383
+ ...result.changes,
384
+ ];
385
+
386
+ const grouped = new Map<string, Change[]>();
387
+ for (const change of allChanges) {
388
+ const list = grouped.get(change.category) ?? [];
389
+ list.push(change);
390
+ grouped.set(change.category, list);
391
+ }
392
+
393
+ const labels: Record<string, string> = {
394
+ stack: "Stack", pattern: "Patterns", convention: "Conventions",
395
+ entity: "Entities", component: "Components", route: "Routes",
396
+ export: "Exports", dependency: "Dependencies",
397
+ };
398
+
399
+ for (const [cat, label] of Object.entries(labels)) {
400
+ const changes = grouped.get(cat);
401
+ if (!changes || changes.length === 0) continue;
402
+
403
+ lines.push(` ${label}`);
404
+ for (const change of changes) {
405
+ const symbol = change.type === "added" ? "+" : change.type === "removed" ? "-" : "~";
406
+ lines.push(` ${symbol} ${change.name}`);
407
+ if (change.details) {
408
+ for (const detail of change.details) {
409
+ lines.push(` ${detail}`);
410
+ }
411
+ }
412
+ }
413
+ lines.push("");
414
+ }
415
+
416
+ if (result.summary.total_changes === 0) {
417
+ lines.push(" No changes detected.\n");
418
+ }
419
+
420
+ return lines.join("\n");
421
+ }
package/src/extract.ts ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Extraction orchestrator — discovers plugins, runs them, merges results.
3
+ *
4
+ * PURE: This module returns a ContextDocument. It does NOT write to disk.
5
+ * I/O is handled by the CLI or calling code.
6
+ */
7
+
8
+ import { execSync } from "child_process";
9
+ import { resolve, basename } from "path";
10
+ import { existsSync, readFileSync } from "fs";
11
+ import { createEmptyDocument, type ContextDocument } from "./schema.js";
12
+ import {
13
+ detectPlugins,
14
+ getPlugins,
15
+ type ExtractorPlugin,
16
+ type ParsedProject,
17
+ type ExtractResult,
18
+ type ProgressCallback,
19
+ } from "./plugin.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Public API
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface ExtractOptions {
26
+ /** Absolute path to the project root */
27
+ projectPath: string;
28
+ /** Specific plugins to run (by name). If empty, auto-detect. */
29
+ plugins?: string[];
30
+ /** Progress callback for reporting extraction status */
31
+ onProgress?: ProgressCallback;
32
+ }
33
+
34
+ /**
35
+ * Extract a ContextDocument from a project.
36
+ *
37
+ * This is the primary API — it discovers applicable plugins,
38
+ * runs them, and merges results into a unified document.
39
+ *
40
+ * Returns a ContextDocument. Does NOT write to disk.
41
+ */
42
+ export async function extractContext(opts: ExtractOptions): Promise<ContextDocument> {
43
+ const startTime = Date.now();
44
+ const projectPath = resolve(opts.projectPath);
45
+ const projectName = inferProjectName(projectPath);
46
+ const progress = opts.onProgress ?? (() => {});
47
+
48
+ progress({ phase: "init", message: `Parsing ${projectName} at ${projectPath}` });
49
+
50
+ // Discover which plugins apply to this project
51
+ progress({ phase: "detect", message: "Detecting project type..." });
52
+
53
+ let plugins: ExtractorPlugin[];
54
+ if (opts.plugins && opts.plugins.length > 0) {
55
+ // Use explicitly specified plugins
56
+ const allPlugins = getPlugins();
57
+ plugins = allPlugins.filter((p) => opts.plugins!.includes(p.name));
58
+ if (plugins.length === 0) {
59
+ throw new Error(
60
+ `No registered plugins match: ${opts.plugins.join(", ")}. ` +
61
+ `Available: ${allPlugins.map((p) => p.name).join(", ") || "none"}`
62
+ );
63
+ }
64
+ } else {
65
+ // Auto-detect
66
+ plugins = await detectPlugins(projectPath);
67
+ if (plugins.length === 0) {
68
+ throw new Error(
69
+ "No plugins detected support for this project. " +
70
+ "Install a parser plugin (e.g., @sigil-engine/parser-typescript)."
71
+ );
72
+ }
73
+ }
74
+
75
+ progress({
76
+ phase: "detect",
77
+ message: `Detected plugins: ${plugins.map((p) => p.name).join(", ")}`,
78
+ });
79
+
80
+ // Create the document
81
+ const doc = createEmptyDocument(projectName);
82
+
83
+ // Run each plugin: init → extract → merge
84
+ for (const plugin of plugins) {
85
+ progress({
86
+ phase: "extract",
87
+ plugin: plugin.name,
88
+ message: `Running ${plugin.name} plugin...`,
89
+ });
90
+
91
+ const project = await plugin.init(projectPath);
92
+
93
+ progress({
94
+ phase: "extract",
95
+ plugin: plugin.name,
96
+ message: `${plugin.name}: found ${project.sourceFiles.length} source files`,
97
+ });
98
+
99
+ const result = await plugin.extract(project);
100
+
101
+ progress({
102
+ phase: "merge",
103
+ plugin: plugin.name,
104
+ message: `Merging ${plugin.name} results...`,
105
+ });
106
+
107
+ mergeResult(doc, result);
108
+ }
109
+
110
+ // Finalize metadata
111
+ const elapsed = Date.now() - startTime;
112
+ doc._extraction = {
113
+ extracted_at: new Date().toISOString(),
114
+ sigil_version: "0.1.0",
115
+ source_commit: getGitCommit(projectPath),
116
+ extraction_ms: elapsed,
117
+ plugins_used: plugins.map((p) => `${p.name}@${p.version}`),
118
+ };
119
+
120
+ progress({
121
+ phase: "done",
122
+ message: `Done in ${elapsed}ms — ${doc.entities.length} entities, ${doc.components.length} components, ${doc.routes.length} routes, ${doc.exports.length} exports`,
123
+ elapsed_ms: elapsed,
124
+ });
125
+
126
+ return doc;
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Merge plugin results into document
131
+ // ---------------------------------------------------------------------------
132
+
133
+ function mergeResult(doc: ContextDocument, result: ExtractResult): void {
134
+ // Arrays: concatenate
135
+ if (result.entities) doc.entities.push(...result.entities);
136
+ if (result.components) doc.components.push(...result.components);
137
+ if (result.routes) doc.routes.push(...result.routes);
138
+ if (result.exports) doc.exports.push(...result.exports);
139
+ if (result.call_graph) doc.call_graph.push(...result.call_graph);
140
+ if (result.component_graph) doc.component_graph.push(...result.component_graph);
141
+ if (result.env_vars) doc.env_vars.push(...result.env_vars);
142
+ if (result.dependencies) doc.dependencies.push(...result.dependencies);
143
+
144
+ // Records: merge (later plugins can override earlier ones)
145
+ if (result.stack) Object.assign(doc.stack, result.stack);
146
+ if (result.patterns) Object.assign(doc.patterns, result.patterns);
147
+ if (result.conventions) Object.assign(doc.conventions, result.conventions);
148
+
149
+ // Extensions: deep merge
150
+ if (result.extensions) {
151
+ doc.extensions = doc.extensions ?? {};
152
+ Object.assign(doc.extensions, result.extensions);
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Deduplication — remove exports already captured as components/entities
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export function deduplicateExports(doc: ContextDocument): void {
161
+ const componentNames = new Set(doc.components.map((c) => c.name));
162
+ const entitySymbols = new Set(doc.entities.map((e) => e._meta.symbol));
163
+ doc.exports = doc.exports.filter(
164
+ (exp) => !componentNames.has(exp.name) && !entitySymbols.has(exp.name),
165
+ );
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Helpers
170
+ // ---------------------------------------------------------------------------
171
+
172
+ function inferProjectName(projectPath: string): string {
173
+ const pkgPath = resolve(projectPath, "package.json");
174
+ if (existsSync(pkgPath)) {
175
+ try {
176
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
177
+ if (pkg.name && !pkg.name.startsWith("@")) return pkg.name;
178
+ if (pkg.name) return pkg.name.split("/").pop() ?? basename(projectPath);
179
+ } catch {
180
+ // fall through
181
+ }
182
+ }
183
+ return basename(projectPath);
184
+ }
185
+
186
+ function getGitCommit(projectPath: string): string | undefined {
187
+ try {
188
+ return execSync("git rev-parse --short HEAD", {
189
+ cwd: projectPath,
190
+ encoding: "utf-8",
191
+ stdio: ["pipe", "pipe", "pipe"],
192
+ }).trim();
193
+ } catch {
194
+ return undefined;
195
+ }
196
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @sigil-engine/core — Public API
3
+ *
4
+ * The core engine for Sigil. Language-agnostic schema, plugin system,
5
+ * extraction orchestrator, diff engine, and serialization.
6
+ *
7
+ * Usage:
8
+ * import { extractContext, diffContexts, registerPlugin } from '@sigil-engine/core';
9
+ *
10
+ * registerPlugin(typescriptPlugin);
11
+ * const doc = await extractContext({ projectPath: './my-app' });
12
+ */
13
+
14
+ // Schema types
15
+ export type {
16
+ ContextDocument,
17
+ Meta,
18
+ MetaType,
19
+ StackInfo,
20
+ ExtractionMeta,
21
+ Entity,
22
+ EntityField,
23
+ EntityRelation,
24
+ Component,
25
+ ComponentProp,
26
+ Route,
27
+ RouteMethod,
28
+ ExportedSymbol,
29
+ Patterns,
30
+ Conventions,
31
+ Dependency,
32
+ CallEdge,
33
+ ComponentEdge,
34
+ EnvVar,
35
+ } from "./schema.js";
36
+
37
+ export { createEmptyDocument } from "./schema.js";
38
+
39
+ // Plugin system
40
+ export type {
41
+ ExtractorPlugin,
42
+ ParsedProject,
43
+ ExtractResult,
44
+ ApplyIntent,
45
+ ApplyResult,
46
+ IntentType,
47
+ IntentConfidence,
48
+ ProgressCallback,
49
+ ProgressEvent,
50
+ ProgressPhase,
51
+ } from "./plugin.js";
52
+
53
+ export {
54
+ registerPlugin,
55
+ getPlugins,
56
+ detectPlugins,
57
+ clearPlugins,
58
+ } from "./plugin.js";
59
+
60
+ // Extraction
61
+ export type { ExtractOptions } from "./extract.js";
62
+ export { extractContext, deduplicateExports } from "./extract.js";
63
+
64
+ // Diff
65
+ export type { DiffResult, Change, ChangeType, ChangeCategory } from "./diff.js";
66
+ export { diffContexts, formatDiff } from "./diff.js";
67
+
68
+ // Serialization
69
+ export {
70
+ serializeYaml,
71
+ serializeJson,
72
+ deserializeYaml,
73
+ deserializeJson,
74
+ writeContextFile,
75
+ readContextFile,
76
+ } from "./serialize.js";
77
+
78
+ // Cache
79
+ export type { FileCache, ChangedFiles } from "./cache.js";
80
+ export {
81
+ readCache,
82
+ writeCache,
83
+ buildCacheFromFiles,
84
+ getChangedFiles,
85
+ } from "./cache.js";
86
+
87
+ // Safety (apply module)
88
+ export type { SafetyCheckpoint, ValidationResult, ConfirmCallback } from "./safety.js";
89
+ export {
90
+ preApplySafety,
91
+ postApplyValidation,
92
+ rollbackApply,
93
+ getGitStatus,
94
+ } from "./safety.js";