autonomous-flow-daemon 1.1.0 → 1.6.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. package/src/version.ts +15 -0
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Incremental Hologram — diff-only mode.
3
+ *
4
+ * Compares previous and current hologram extractions,
5
+ * returns only changed nodes with surrounding context in unified-diff style.
6
+ */
7
+
8
+ import type { Tree } from "web-tree-sitter";
9
+ import type { HologramResult, LanguageExtractor, HologramOptions } from "./types";
10
+ import { TreeSitterEngine } from "./engine";
11
+
12
+ /** In-memory cache for previous hologram lines (per file path) */
13
+ const hologramCache = new Map<string, string[]>();
14
+
15
+ /** Maximum cache entries */
16
+ const MAX_CACHE_SIZE = 200;
17
+
18
+ export function clearHologramCache(): void {
19
+ hologramCache.clear();
20
+ }
21
+
22
+ export function setCachedHologram(filePath: string, lines: string[]): void {
23
+ // True LRU: delete existing entry first so re-insert moves it to end
24
+ hologramCache.delete(filePath);
25
+ if (hologramCache.size >= MAX_CACHE_SIZE) {
26
+ const oldestKey = hologramCache.keys().next().value;
27
+ if (oldestKey) hologramCache.delete(oldestKey);
28
+ }
29
+ hologramCache.set(filePath, lines);
30
+ }
31
+
32
+ export function getCachedHologram(filePath: string): string[] | undefined {
33
+ const value = hologramCache.get(filePath);
34
+ if (value !== undefined) {
35
+ // Promote to most-recently-used position
36
+ hologramCache.delete(filePath);
37
+ hologramCache.set(filePath, value);
38
+ }
39
+ return value;
40
+ }
41
+
42
+ /**
43
+ * Generate an incremental (diff-only) hologram.
44
+ * Compares current extraction with cached previous result.
45
+ * Returns unified-diff style output showing only changed nodes.
46
+ */
47
+ export async function generateIncrementalHologram(
48
+ filePath: string,
49
+ source: string,
50
+ extractor: LanguageExtractor,
51
+ options?: HologramOptions,
52
+ ): Promise<HologramResult> {
53
+ const engine = await TreeSitterEngine.getInstance();
54
+ const tree = await engine.parse(source, extractor.grammarName);
55
+ const currentLines = extractor.extract(tree, source, options);
56
+ tree.delete();
57
+
58
+ const previousLines = getCachedHologram(filePath);
59
+
60
+ // Cache current result for next diff
61
+ setCachedHologram(filePath, currentLines);
62
+
63
+ // No previous → return full hologram with diff header
64
+ if (!previousLines) {
65
+ const hologram = currentLines.join("\n");
66
+ return {
67
+ hologram,
68
+ originalLength: source.length,
69
+ hologramLength: hologram.length,
70
+ savings: source.length > 0
71
+ ? Math.round((source.length - hologram.length) / source.length * 1000) / 10
72
+ : 0,
73
+ language: extractor.grammarName,
74
+ isDiff: false,
75
+ changedNodes: currentLines.length,
76
+ };
77
+ }
78
+
79
+ // Diff previous vs current lines
80
+ const diffOutput = buildUnifiedDiff(filePath, previousLines, currentLines);
81
+
82
+ return {
83
+ hologram: diffOutput.text,
84
+ originalLength: source.length,
85
+ hologramLength: diffOutput.text.length,
86
+ savings: source.length > 0
87
+ ? Math.round((source.length - diffOutput.text.length) / source.length * 1000) / 10
88
+ : 0,
89
+ language: extractor.grammarName,
90
+ isDiff: true,
91
+ changedNodes: diffOutput.changedCount,
92
+ };
93
+ }
94
+
95
+ interface DiffOutput {
96
+ text: string;
97
+ changedCount: number;
98
+ }
99
+
100
+ /**
101
+ * Build a unified-diff style output comparing old and new hologram lines.
102
+ * Groups unchanged lines into summary markers, shows changed lines with +/- prefixes.
103
+ */
104
+ function buildUnifiedDiff(filePath: string, oldLines: string[], newLines: string[]): DiffOutput {
105
+ const header = `--- a/${filePath} (previous)\n+++ b/${filePath} (current)\n`;
106
+
107
+ // Simple line-by-line diff using LCS approach
108
+ const hunks = computeHunks(oldLines, newLines);
109
+
110
+ if (hunks.length === 0) {
111
+ return {
112
+ text: header + "@@ no changes @@",
113
+ changedCount: 0,
114
+ };
115
+ }
116
+
117
+ const parts: string[] = [header];
118
+ let changedCount = 0;
119
+ let oldIdx = 0;
120
+
121
+ for (const hunk of hunks) {
122
+ // Show unchanged lines before this hunk as a summary
123
+ const unchangedBefore = hunk.oldStart - oldIdx;
124
+ if (unchangedBefore > 0) {
125
+ const summaryLines = oldLines.slice(oldIdx, hunk.oldStart);
126
+ const firstLine = summaryLines[0]?.split("{")[0]?.trim() ?? "...";
127
+ parts.push(`@@ unchanged: ${firstLine} (${unchangedBefore} ${unchangedBefore === 1 ? "declaration" : "declarations"}) @@`);
128
+ }
129
+
130
+ // Show removed lines
131
+ for (let i = hunk.oldStart; i < hunk.oldStart + hunk.oldCount; i++) {
132
+ parts.push(`- ${oldLines[i]}`);
133
+ changedCount++;
134
+ }
135
+
136
+ // Show added lines
137
+ for (let i = hunk.newStart; i < hunk.newStart + hunk.newCount; i++) {
138
+ parts.push(`+ ${newLines[i]}`);
139
+ }
140
+
141
+ oldIdx = hunk.oldStart + hunk.oldCount;
142
+ }
143
+
144
+ // Trailing unchanged
145
+ const trailingCount = oldLines.length - oldIdx;
146
+ if (trailingCount > 0) {
147
+ parts.push(`@@ unchanged: ${trailingCount} more ${trailingCount === 1 ? "declaration" : "declarations"} @@`);
148
+ }
149
+
150
+ return { text: parts.join("\n"), changedCount };
151
+ }
152
+
153
+ interface Hunk {
154
+ oldStart: number;
155
+ oldCount: number;
156
+ newStart: number;
157
+ newCount: number;
158
+ }
159
+
160
+ /**
161
+ * Compute diff hunks between old and new line arrays.
162
+ * Uses a simple O(n*m) LCS-based diff suitable for small arrays (hologram lines are typically < 100).
163
+ */
164
+ function computeHunks(oldLines: string[], newLines: string[]): Hunk[] {
165
+ const n = oldLines.length;
166
+ const m = newLines.length;
167
+
168
+ // Guard: for very large inputs, fall back to full diff to stay within SEAM budget
169
+ if (n * m > 50_000) {
170
+ return [{ oldStart: 0, oldCount: n, newStart: 0, newCount: m }];
171
+ }
172
+
173
+ // Build LCS table
174
+ const dp: number[][] = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
175
+ for (let i = 1; i <= n; i++) {
176
+ for (let j = 1; j <= m; j++) {
177
+ if (oldLines[i - 1] === newLines[j - 1]) {
178
+ dp[i][j] = dp[i - 1][j - 1] + 1;
179
+ } else {
180
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
181
+ }
182
+ }
183
+ }
184
+
185
+ // Backtrack to find matching lines
186
+ const matches: Array<[number, number]> = [];
187
+ let i = n, j = m;
188
+ while (i > 0 && j > 0) {
189
+ if (oldLines[i - 1] === newLines[j - 1]) {
190
+ matches.unshift([i - 1, j - 1]);
191
+ i--; j--;
192
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
193
+ i--;
194
+ } else {
195
+ j--;
196
+ }
197
+ }
198
+
199
+ // Convert matches to hunks (gaps between matches)
200
+ const hunks: Hunk[] = [];
201
+ let oi = 0, ni = 0;
202
+
203
+ for (const [mi, mj] of matches) {
204
+ if (oi < mi || ni < mj) {
205
+ hunks.push({
206
+ oldStart: oi,
207
+ oldCount: mi - oi,
208
+ newStart: ni,
209
+ newCount: mj - ni,
210
+ });
211
+ }
212
+ oi = mi + 1;
213
+ ni = mj + 1;
214
+ }
215
+
216
+ // Trailing diff
217
+ if (oi < n || ni < m) {
218
+ hunks.push({
219
+ oldStart: oi,
220
+ oldCount: n - oi,
221
+ newStart: ni,
222
+ newCount: m - ni,
223
+ });
224
+ }
225
+
226
+ return hunks;
227
+ }
@@ -0,0 +1,132 @@
1
+ import type { Node, Tree } from "web-tree-sitter";
2
+ import type { LanguageExtractor, HologramOptions } from "./types";
3
+
4
+ /** Stub a Python function/method body — keep signature + "..." */
5
+ function stubPythonBody(node: Node, source: string): string {
6
+ const body = node.childForFieldName("body");
7
+ if (!body) return node.text;
8
+ return source.slice(node.startIndex, body.startIndex).trimEnd() + " ...";
9
+ }
10
+
11
+ /** Extract a Python function definition */
12
+ function extractFunction(node: Node, source: string): string {
13
+ // Decorators
14
+ const decorators = collectDecorators(node, source);
15
+ const sig = stubPythonBody(node, source);
16
+ return decorators + sig;
17
+ }
18
+
19
+ /** Extract a Python class with method signatures */
20
+ function extractClass(node: Node, source: string): string {
21
+ const decorators = collectDecorators(node, source);
22
+ const nameNode = node.childForFieldName("name");
23
+ const name = nameNode?.text ?? "?";
24
+ const superclasses = node.childForFieldName("superclasses");
25
+ const sup = superclasses ? superclasses.text : "";
26
+ const body = node.childForFieldName("body");
27
+
28
+ const header = `class ${name}${sup ? `(${sup.replace(/^\(|\)$/g, "")})` : ""}:`;
29
+
30
+ if (!body) return decorators + header;
31
+
32
+ const members: string[] = [];
33
+ for (const child of body.namedChildren) {
34
+ switch (child.type) {
35
+ case "function_definition": {
36
+ const memberDecorators = collectDecorators(child, source);
37
+ members.push(indent(memberDecorators + stubPythonBody(child, source)));
38
+ break;
39
+ }
40
+ case "expression_statement": {
41
+ // Type-annotated assignments: x: int = 42
42
+ const expr = child.namedChildren[0];
43
+ if (expr?.type === "assignment" || expr?.type === "type") {
44
+ members.push(indent(child.text.split("\n")[0]));
45
+ }
46
+ break;
47
+ }
48
+ case "class_definition": {
49
+ // Nested class — just show header
50
+ const nestedName = child.childForFieldName("name")?.text ?? "?";
51
+ members.push(indent(`class ${nestedName}: ...`));
52
+ break;
53
+ }
54
+ }
55
+ }
56
+
57
+ if (members.length === 0) {
58
+ return decorators + header + "\n ...";
59
+ }
60
+
61
+ return decorators + header + "\n" + members.join("\n");
62
+ }
63
+
64
+ /** Collect decorator lines above a node */
65
+ function collectDecorators(node: Node, source: string): string {
66
+ const decorators: string[] = [];
67
+ // In tree-sitter-python, decorators are children of the decorated_definition
68
+ // or are 'decorator' type children of the function/class
69
+ const parent = node.parent;
70
+ if (parent?.type === "decorated_definition") {
71
+ for (const child of parent.namedChildren) {
72
+ if (child.type === "decorator") {
73
+ decorators.push(child.text);
74
+ }
75
+ }
76
+ }
77
+ return decorators.length > 0 ? decorators.join("\n") + "\n" : "";
78
+ }
79
+
80
+ function indent(s: string): string {
81
+ return s.split("\n").map(line => " " + line).join("\n");
82
+ }
83
+
84
+ /** Process a single top-level statement */
85
+ function extractTopLevel(node: Node, source: string): string | null {
86
+ switch (node.type) {
87
+ case "import_statement":
88
+ case "import_from_statement":
89
+ return node.text;
90
+ case "function_definition":
91
+ return extractFunction(node, source);
92
+ case "class_definition":
93
+ return extractClass(node, source);
94
+ case "decorated_definition": {
95
+ // Unwrap to get the inner function/class
96
+ const inner = node.namedChildren.find(c =>
97
+ c.type === "function_definition" || c.type === "class_definition");
98
+ if (inner) return extractTopLevel(inner, source);
99
+ return null;
100
+ }
101
+ case "expression_statement": {
102
+ // Module-level type annotations or assignments
103
+ const expr = node.namedChildren[0];
104
+ if (expr?.type === "assignment" || expr?.type === "type") {
105
+ return node.text.split("\n")[0];
106
+ }
107
+ // __all__ = [...]
108
+ if (node.text.startsWith("__all__")) {
109
+ return node.text;
110
+ }
111
+ return null;
112
+ }
113
+ default:
114
+ return null;
115
+ }
116
+ }
117
+
118
+ export const pyExtractor: LanguageExtractor = {
119
+ extensions: ["py", "pyi"],
120
+ grammarName: "python",
121
+
122
+ extract(tree: Tree, source: string, _options?: HologramOptions): string[] {
123
+ const lines: string[] = [];
124
+
125
+ for (const stmt of tree.rootNode.namedChildren) {
126
+ const line = extractTopLevel(stmt, source);
127
+ if (line) lines.push(line);
128
+ }
129
+
130
+ return lines;
131
+ },
132
+ };
@@ -0,0 +1,320 @@
1
+ import type { Node, Tree } from "web-tree-sitter";
2
+ import type { LanguageExtractor, HologramOptions } from "./types";
3
+ import { readFileSync } from "fs";
4
+
5
+ /** Extract imported symbols from a context file using regex (L1 filtering) */
6
+ function extractImportedSymbols(contextSource: string, targetPath: string): Set<string> | "all" {
7
+ const symbols = new Set<string>();
8
+ const targetBase = targetPath.replace(/\.[tj]sx?$/, "").replace(/\\/g, "/");
9
+ const targetName = targetBase.split("/").pop() ?? targetBase;
10
+
11
+ function matchesTarget(from: string): boolean {
12
+ const normalized = from.replace(/\.[tj]sx?$/, "").replace(/\\/g, "/");
13
+ return normalized.endsWith(targetName) || normalized.endsWith(targetBase);
14
+ }
15
+
16
+ // Namespace import
17
+ const nsRe = /import\s+\*\s+as\s+\w+\s+from\s+["']([^"']+)["']/g;
18
+ for (const m of contextSource.matchAll(nsRe)) {
19
+ if (matchesTarget(m[1])) return "all";
20
+ }
21
+
22
+ // Named imports
23
+ const namedRe = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
24
+ for (const m of contextSource.matchAll(namedRe)) {
25
+ if (matchesTarget(m[2])) {
26
+ m[1].split(",").forEach(s => {
27
+ const name = s.trim().split(/\s+as\s+/)[0].trim();
28
+ if (name) symbols.add(name);
29
+ });
30
+ }
31
+ }
32
+
33
+ // Default import
34
+ const defaultRe = /import\s+(\w+)\s+from\s+["']([^"']+)["']/g;
35
+ for (const m of contextSource.matchAll(defaultRe)) {
36
+ if (matchesTarget(m[2])) symbols.add("default");
37
+ }
38
+
39
+ // Combined: import X, { A, B } from "./target"
40
+ const combinedRe = /import\s+(\w+)\s*,\s*\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
41
+ for (const m of contextSource.matchAll(combinedRe)) {
42
+ if (matchesTarget(m[3])) {
43
+ symbols.add("default");
44
+ m[2].split(",").forEach(s => {
45
+ const name = s.trim().split(/\s+as\s+/)[0].trim();
46
+ if (name) symbols.add(name);
47
+ });
48
+ }
49
+ }
50
+
51
+ return symbols;
52
+ }
53
+
54
+ /** Get the exported name from a top-level declaration node */
55
+ function getExportedName(node: Node): string | null {
56
+ // Check if wrapped in export_statement
57
+ const parent = node.parent;
58
+ const isExported = parent?.type === "export_statement" || node.type === "export_statement";
59
+ if (!isExported && parent?.type !== "program") return null;
60
+ if (!isExported) return null;
61
+
62
+ const decl = node.type === "export_statement"
63
+ ? node.namedChildren.find(c =>
64
+ c.type === "function_declaration" ||
65
+ c.type === "class_declaration" ||
66
+ c.type === "interface_declaration" ||
67
+ c.type === "type_alias_declaration" ||
68
+ c.type === "enum_declaration" ||
69
+ c.type === "lexical_declaration")
70
+ : node;
71
+
72
+ if (!decl) return null;
73
+
74
+ const nameNode = decl.childForFieldName("name");
75
+ if (nameNode) return nameNode.text;
76
+
77
+ // Variable declarations
78
+ if (decl.type === "lexical_declaration") {
79
+ const declarator = decl.namedChildren.find(c => c.type === "variable_declarator");
80
+ return declarator?.childForFieldName("name")?.text ?? null;
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ /** Stub a node's body, keeping only the signature */
87
+ function stubBody(node: Node, source: string): string {
88
+ const body = node.childForFieldName("body");
89
+ if (!body) return collapseWhitespace(node.text);
90
+ return collapseWhitespace(source.slice(node.startIndex, body.startIndex).trimEnd()) + " {…}";
91
+ }
92
+
93
+ /** Extract function signature */
94
+ function extractFunction(node: Node, source: string): string {
95
+ return stubBody(node, source);
96
+ }
97
+
98
+ /** Extract class with member signatures */
99
+ function extractClass(node: Node, source: string): string {
100
+ const nameNode = node.childForFieldName("name");
101
+ const name = nameNode?.text ?? "Anonymous";
102
+ const body = node.childForFieldName("body");
103
+
104
+ // Heritage (extends/implements)
105
+ let heritage = "";
106
+ const heritageNodes = node.children.filter(c =>
107
+ c.type === "extends_clause" || c.type === "implements_clause" ||
108
+ c.type === "class_heritage");
109
+ if (heritageNodes.length > 0) {
110
+ heritage = " " + heritageNodes.map(h => collapseWhitespace(h.text)).join(" ");
111
+ }
112
+
113
+ // Prefix (export, abstract, etc.)
114
+ const prefix = collapseWhitespace(
115
+ source.slice(node.startIndex, (nameNode ?? body ?? node).startIndex).trimEnd()
116
+ ).replace(name, "").trimEnd();
117
+ const classPrefix = prefix ? `${prefix} ${name}` : `class ${name}`;
118
+
119
+ if (!body) return `${classPrefix}${heritage} {}`;
120
+
121
+ const members: string[] = [];
122
+ for (const member of body.namedChildren) {
123
+ switch (member.type) {
124
+ case "public_field_definition":
125
+ case "property_definition": {
126
+ members.push(" " + collapseWhitespace(member.text).replace(/;$/, "") + ";");
127
+ break;
128
+ }
129
+ case "method_definition": {
130
+ const methodBody = member.childForFieldName("body");
131
+ if (methodBody) {
132
+ const sig = collapseWhitespace(source.slice(member.startIndex, methodBody.startIndex).trimEnd());
133
+ members.push(" " + sig + ";");
134
+ } else {
135
+ members.push(" " + collapseWhitespace(member.text) + ";");
136
+ }
137
+ break;
138
+ }
139
+ }
140
+ }
141
+
142
+ return `${classPrefix}${heritage} {\n${members.join("\n")}\n}`;
143
+ }
144
+
145
+ /** Extract interface with all members */
146
+ function extractInterface(node: Node, source: string): string {
147
+ const nameNode = node.childForFieldName("name");
148
+ const name = nameNode?.text ?? "Anonymous";
149
+ const body = node.childForFieldName("body");
150
+
151
+ // Heritage (extends)
152
+ const extendsClause = node.children.find(c => c.type === "extends_type_clause");
153
+ const ext = extendsClause ? " " + collapseWhitespace(extendsClause.text) : "";
154
+
155
+ // Prefix
156
+ const prefixEnd = (nameNode ?? body ?? node).startIndex;
157
+ const rawPrefix = source.slice(node.startIndex, prefixEnd).trimEnd();
158
+ const prefix = collapseWhitespace(rawPrefix).replace(name, "").trimEnd();
159
+ const ifacePrefix = prefix ? `${prefix} ${name}` : `interface ${name}`;
160
+
161
+ if (!body) return `${ifacePrefix}${ext} {}`;
162
+
163
+ const members = body.namedChildren.map(m => {
164
+ const text = collapseWhitespace(m.text).replace(/;$/, "");
165
+ return " " + text + ";";
166
+ });
167
+
168
+ return `${ifacePrefix}${ext} {\n${members.join("\n")}\n}`;
169
+ }
170
+
171
+ /** Extract enum */
172
+ function extractEnum(node: Node): string {
173
+ const nameNode = node.childForFieldName("name");
174
+ const name = nameNode?.text ?? "Anonymous";
175
+ const body = node.childForFieldName("body");
176
+
177
+ const isExport = node.parent?.type === "export_statement" ? "export " : "";
178
+ const isConst = node.children.some(c => c.text === "const") ? "const " : "";
179
+
180
+ if (!body) return `${isExport}${isConst}enum ${name} {}`;
181
+
182
+ const members = body.namedChildren
183
+ .filter(m => m.type === "enum_member" || m.type === "property_identifier")
184
+ .map(m => collapseWhitespace(m.text));
185
+
186
+ return `${isExport}${isConst}enum ${name} { ${members.join(", ")} }`;
187
+ }
188
+
189
+ /** Extract variable statement (const/let/var with possible arrow functions) */
190
+ function extractVariable(node: Node, source: string): string {
191
+ const keyword = node.children[0]?.text ?? "const";
192
+ const isExport = node.parent?.type === "export_statement" ? "export " : "";
193
+
194
+ const declarators = node.namedChildren.filter(c => c.type === "variable_declarator");
195
+ const parts = declarators.map(d => {
196
+ const name = d.childForFieldName("name")?.text ?? "?";
197
+ const typeAnn = d.childForFieldName("type")
198
+ ? ": " + collapseWhitespace(d.childForFieldName("type")!.text)
199
+ : "";
200
+ const value = d.childForFieldName("value");
201
+
202
+ if (value && (value.type === "arrow_function" || value.type === "function_expression" || value.type === "function")) {
203
+ return `${name} = ${stubBody(value, source)}`;
204
+ }
205
+ if (typeAnn) return `${name}${typeAnn}`;
206
+ if (value) return `${name} = …`;
207
+ return name;
208
+ });
209
+
210
+ return `${isExport}${keyword} ${parts.join(", ")};`;
211
+ }
212
+
213
+ /** Extract type alias */
214
+ function extractTypeAlias(node: Node): string {
215
+ const isExport = node.parent?.type === "export_statement" ? "export " : "";
216
+ return isExport + collapseWhitespace(node.text);
217
+ }
218
+
219
+ function collapseWhitespace(s: string): string {
220
+ return s.replace(/\s+/g, " ").trim();
221
+ }
222
+
223
+ /** Process a single top-level statement */
224
+ function extractTopLevel(node: Node, source: string): string | null {
225
+ // Unwrap export_statement to get the inner declaration
226
+ if (node.type === "export_statement") {
227
+ // Re-export: export { ... } from "..."
228
+ const exportClause = node.namedChildren.find(c => c.type === "export_clause");
229
+ if (exportClause) return collapseWhitespace(node.text);
230
+
231
+ // export default expression
232
+ const defaultKw = node.children.find(c => c.text === "default");
233
+ if (defaultKw) {
234
+ const inner = node.namedChildren.find(c => c.type !== "export_clause");
235
+ if (inner && (inner.type === "function_declaration" || inner.type === "class_declaration")) {
236
+ return "export default " + extractTopLevel(inner, source);
237
+ }
238
+ return `export default …;`;
239
+ }
240
+
241
+ // export <declaration>
242
+ const inner = node.namedChildren[0];
243
+ if (inner) {
244
+ const result = extractTopLevel(inner, source);
245
+ if (result) {
246
+ const alreadyHasExport = result.startsWith("export ");
247
+ return alreadyHasExport ? result : "export " + result;
248
+ }
249
+ }
250
+ return collapseWhitespace(node.text);
251
+ }
252
+
253
+ switch (node.type) {
254
+ case "import_statement":
255
+ return collapseWhitespace(node.text);
256
+ case "function_declaration":
257
+ case "generator_function_declaration":
258
+ return extractFunction(node, source);
259
+ case "class_declaration":
260
+ return extractClass(node, source);
261
+ case "interface_declaration":
262
+ return extractInterface(node, source);
263
+ case "type_alias_declaration":
264
+ return extractTypeAlias(node);
265
+ case "enum_declaration":
266
+ return extractEnum(node);
267
+ case "lexical_declaration":
268
+ case "variable_declaration":
269
+ return extractVariable(node, source);
270
+ case "export_statement":
271
+ return collapseWhitespace(node.text);
272
+ default:
273
+ return null;
274
+ }
275
+ }
276
+
277
+ export const tsExtractor: LanguageExtractor = {
278
+ extensions: ["ts", "tsx", "js", "jsx", "mts", "cts"],
279
+ grammarName: "typescript",
280
+
281
+ extract(tree: Tree, source: string, options?: HologramOptions): string[] {
282
+ const lines: string[] = [];
283
+
284
+ // L1 filtering setup
285
+ let importedSymbols: Set<string> | "all" | null = null;
286
+ if (options?.contextFile) {
287
+ try {
288
+ const contextSource = readFileSync(options.contextFile, "utf-8");
289
+ importedSymbols = extractImportedSymbols(contextSource, "");
290
+ if (importedSymbols !== "all" && importedSymbols.size === 0) importedSymbols = null;
291
+ } catch {
292
+ importedSymbols = null;
293
+ }
294
+ }
295
+
296
+ for (const stmt of tree.rootNode.namedChildren) {
297
+ // L1: filter non-imported exports
298
+ if (importedSymbols && importedSymbols !== "all") {
299
+ const exportedName = getExportedName(stmt);
300
+ if (exportedName !== null && !importedSymbols.has(exportedName)) {
301
+ const line = extractTopLevel(stmt, source);
302
+ if (line) {
303
+ const stub = line.split("\n")[0].replace(/\{[^}]*\}?\s*$/, "").trimEnd();
304
+ lines.push(`${stub} // details omitted — read directly if needed`);
305
+ }
306
+ continue;
307
+ }
308
+ }
309
+
310
+ const line = extractTopLevel(stmt, source);
311
+ if (line) lines.push(line);
312
+ }
313
+
314
+ if (importedSymbols && importedSymbols !== "all") {
315
+ lines.push("\n// [afd L1] Non-imported exports are shown as stubs. Use afd_read for full details.");
316
+ }
317
+
318
+ return lines;
319
+ },
320
+ };
@@ -0,0 +1,25 @@
1
+ import type { Tree } from "web-tree-sitter";
2
+
3
+ export interface HologramResult {
4
+ hologram: string;
5
+ originalLength: number;
6
+ hologramLength: number;
7
+ savings: number; // percentage 0-100
8
+ language?: string;
9
+ isDiff?: boolean;
10
+ changedNodes?: number;
11
+ }
12
+
13
+ export interface HologramOptions {
14
+ contextFile?: string;
15
+ diffOnly?: boolean;
16
+ }
17
+
18
+ export interface LanguageExtractor {
19
+ /** Supported file extensions (without dot) */
20
+ extensions: string[];
21
+ /** Tree-sitter grammar name for WASM resolution */
22
+ grammarName: string;
23
+ /** Extract type signatures from AST */
24
+ extract(tree: Tree, source: string, options?: HologramOptions): string[];
25
+ }