autonomous-flow-daemon 1.6.0 → 1.9.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.
- package/CHANGELOG.md +85 -85
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -266
- package/mcp-config.json +10 -10
- package/package.json +4 -2
- package/src/adapters/index.ts +370 -370
- package/src/cli.ts +162 -127
- package/src/commands/benchmark.ts +187 -187
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/evolution.ts +84 -1
- package/src/commands/fix.ts +158 -158
- package/src/commands/lang.ts +41 -41
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -14
- package/src/commands/score.ts +276 -276
- package/src/commands/start.ts +155 -155
- package/src/commands/status.ts +157 -157
- package/src/commands/stop.ts +68 -68
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +329 -16
- package/src/constants.ts +32 -32
- package/src/core/boast.ts +280 -280
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -117
- package/src/core/discovery.ts +65 -65
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -71
- package/src/core/hologram/fallback.ts +11 -11
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -227
- package/src/core/hologram/py-extractor.ts +132 -132
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -320
- package/src/core/hologram/types.ts +27 -25
- package/src/core/hologram.ts +73 -71
- package/src/core/i18n/messages.ts +309 -309
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -33
- package/src/core/log-utils.ts +38 -38
- package/src/core/lru-map.ts +61 -61
- package/src/core/notify.ts +74 -74
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -28
- package/src/daemon/client.ts +78 -65
- package/src/daemon/event-batcher.ts +108 -108
- package/src/daemon/guards.ts +13 -13
- package/src/daemon/http-routes.ts +376 -293
- package/src/daemon/mcp-handler.ts +575 -270
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -590
- package/src/daemon/types.ts +121 -100
- package/src/daemon/workspace-map.ts +104 -92
- package/src/platform.ts +60 -60
- package/src/version.ts +15 -15
- package/README.ko.md +0 -266
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation types and scope resolution for Team Antibody Federation (v1.7).
|
|
3
|
+
*
|
|
4
|
+
* Namespace: "<scope>/<id>" (e.g. "myorg/missing-import-guard")
|
|
5
|
+
* Versioning: monotonic integer + ISO-8601 timestamp (LWW tiebreak)
|
|
6
|
+
* Conflict policy: Remote-Wins-If-Newer (RWIN) — higher version wins;
|
|
7
|
+
* on tie, newer updatedAt wins; on tie, keep local.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface FederatedAntibody {
|
|
11
|
+
/** Local name portion, e.g. "missing-import-guard" */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Namespace slug, e.g. "myorg". Defaults to "local" for non-federated antibodies. */
|
|
14
|
+
scope: string;
|
|
15
|
+
/** Globally unique: "<scope>/<id>" */
|
|
16
|
+
fqid: string;
|
|
17
|
+
patternType: string;
|
|
18
|
+
fileTarget: string;
|
|
19
|
+
patches: { op: string; path: string; value?: string }[];
|
|
20
|
+
/** Monotonic integer starting at 1; incremented on each content change */
|
|
21
|
+
version: number;
|
|
22
|
+
/** ISO 8601 — last mutation time (used as LWW tiebreaker) */
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
/** ISO 8601 — original learning time (immutable) */
|
|
25
|
+
learnedAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FederatedPayload {
|
|
29
|
+
/** Payload format version, e.g. "1.7" */
|
|
30
|
+
version: string;
|
|
31
|
+
generatedAt: string;
|
|
32
|
+
ecosystem: string;
|
|
33
|
+
/** Scope of the publishing team/repo */
|
|
34
|
+
scope: string;
|
|
35
|
+
antibodyCount: number;
|
|
36
|
+
antibodies: FederatedAntibody[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the local scope for this workspace:
|
|
41
|
+
* 1. AFD_SCOPE env var (explicit override)
|
|
42
|
+
* 2. Git remote origin → org slug
|
|
43
|
+
* 3. Fallback: "local"
|
|
44
|
+
*/
|
|
45
|
+
export function resolveScope(): string {
|
|
46
|
+
const envScope = process.env.AFD_SCOPE?.trim();
|
|
47
|
+
if (envScope) return slugify(envScope);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const proc = Bun.spawnSync(["git", "remote", "get-url", "origin"], { stderr: "ignore" });
|
|
51
|
+
if (proc.exitCode === 0) {
|
|
52
|
+
const remoteUrl = new TextDecoder().decode(proc.stdout).trim();
|
|
53
|
+
return parseGitRemoteScope(remoteUrl) || "local";
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// no git binary or no remote configured
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return "local";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseGitRemoteScope(remoteUrl: string): string {
|
|
63
|
+
try {
|
|
64
|
+
if (remoteUrl.startsWith("git@")) {
|
|
65
|
+
// git@github.com:myorg/repo.git
|
|
66
|
+
const colonPart = remoteUrl.split(":")[1] ?? "";
|
|
67
|
+
return slugify(colonPart.split("/")[0]);
|
|
68
|
+
}
|
|
69
|
+
const u = new URL(remoteUrl);
|
|
70
|
+
const firstSegment = u.pathname.replace(/^\//, "").split("/")[0] ?? "";
|
|
71
|
+
return slugify(firstSegment);
|
|
72
|
+
} catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function slugify(s: string): string {
|
|
78
|
+
return s.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* RWIN decision: should the incoming (remote) antibody overwrite the local one?
|
|
83
|
+
* Returns true if remote should win.
|
|
84
|
+
*/
|
|
85
|
+
export function remoteWins(
|
|
86
|
+
remoteVersion: number,
|
|
87
|
+
remoteUpdatedAt: string,
|
|
88
|
+
localVersion: number,
|
|
89
|
+
localUpdatedAt: string,
|
|
90
|
+
): boolean {
|
|
91
|
+
if (remoteVersion > localVersion) return true;
|
|
92
|
+
if (remoteVersion < localVersion) return false;
|
|
93
|
+
// Same version — use updatedAt as tiebreaker (newer wins)
|
|
94
|
+
return remoteUpdatedAt > localUpdatedAt;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Conflict arbitration for split-brain scenarios.
|
|
99
|
+
*
|
|
100
|
+
* Called when version AND updatedAt are identical but patch content differs
|
|
101
|
+
* (two daemons learned the same antibody concurrently and produced different patches).
|
|
102
|
+
*
|
|
103
|
+
* Strategy: deterministic lexicographic comparison of serialised patch content.
|
|
104
|
+
* The "larger" content string wins — ensures all peers converge to the same value
|
|
105
|
+
* without coordination, as long as both sides run the same function.
|
|
106
|
+
*
|
|
107
|
+
* Returns true if the remote patch should overwrite the local one.
|
|
108
|
+
*/
|
|
109
|
+
export function arbitrateConflict(remotePatch: string, localPatch: string): boolean {
|
|
110
|
+
return remotePatch > localPatch;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Full conflict resolution combining RWIN + arbitration.
|
|
115
|
+
* Use this as the single decision point in /antibodies/learn.
|
|
116
|
+
*/
|
|
117
|
+
export function shouldAcceptRemote(
|
|
118
|
+
remote: { version: number; updatedAt: string; patch: string },
|
|
119
|
+
local: { version: number; updatedAt: string; patch: string },
|
|
120
|
+
): { accept: boolean; reason: "newer_version" | "newer_timestamp" | "arbitrated" | "local_wins" } {
|
|
121
|
+
if (remote.version > local.version) return { accept: true, reason: "newer_version" };
|
|
122
|
+
if (remote.version < local.version) return { accept: false, reason: "local_wins" };
|
|
123
|
+
// Same version
|
|
124
|
+
if (remote.updatedAt > local.updatedAt) return { accept: true, reason: "newer_timestamp" };
|
|
125
|
+
if (remote.updatedAt < local.updatedAt) return { accept: false, reason: "local_wins" };
|
|
126
|
+
// True split-brain: identical version + timestamp, different content
|
|
127
|
+
if (remote.patch === local.patch) return { accept: false, reason: "local_wins" };
|
|
128
|
+
return { accept: arbitrateConflict(remote.patch, local.patch), reason: "arbitrated" };
|
|
129
|
+
}
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import { Parser, Language, type Tree } from "web-tree-sitter";
|
|
2
|
-
import { resolve, dirname } from "path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Singleton Tree-sitter engine with grammar caching.
|
|
6
|
-
* Parser.init() runs once; grammar WASMs are lazy-loaded and cached in-memory.
|
|
7
|
-
*/
|
|
8
|
-
export class TreeSitterEngine {
|
|
9
|
-
private static instance: TreeSitterEngine | null = null;
|
|
10
|
-
private parser: Parser | null = null;
|
|
11
|
-
private grammarCache = new Map<string, Language>();
|
|
12
|
-
private initPromise: Promise<void> | null = null;
|
|
13
|
-
|
|
14
|
-
static async getInstance(): Promise<TreeSitterEngine> {
|
|
15
|
-
if (!this.instance) {
|
|
16
|
-
this.instance = new TreeSitterEngine();
|
|
17
|
-
await this.instance.init();
|
|
18
|
-
}
|
|
19
|
-
return this.instance;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Reset singleton — for testing only */
|
|
23
|
-
static resetForTest(): void {
|
|
24
|
-
if (this.instance?.parser) {
|
|
25
|
-
this.instance.parser.delete();
|
|
26
|
-
}
|
|
27
|
-
this.instance = null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private async init(): Promise<void> {
|
|
31
|
-
if (this.initPromise) return this.initPromise;
|
|
32
|
-
this.initPromise = (async () => {
|
|
33
|
-
await Parser.init();
|
|
34
|
-
this.parser = new Parser();
|
|
35
|
-
})();
|
|
36
|
-
return this.initPromise;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async parse(source: string, grammarName: string): Promise<Tree> {
|
|
40
|
-
const grammar = await this.loadGrammar(grammarName);
|
|
41
|
-
this.parser!.setLanguage(grammar);
|
|
42
|
-
const tree = this.parser!.parse(source);
|
|
43
|
-
if (!tree) throw new Error(`Failed to parse with grammar: ${grammarName}`);
|
|
44
|
-
return tree;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private async loadGrammar(grammarName: string): Promise<Language> {
|
|
48
|
-
const cached = this.grammarCache.get(grammarName);
|
|
49
|
-
if (cached) return cached;
|
|
50
|
-
|
|
51
|
-
const wasmPath = resolveGrammarWasm(grammarName);
|
|
52
|
-
const lang = await Language.load(wasmPath);
|
|
53
|
-
this.grammarCache.set(grammarName, lang);
|
|
54
|
-
return lang;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Resolve WASM file path from installed npm grammar package */
|
|
59
|
-
function resolveGrammarWasm(grammarName: string): string {
|
|
60
|
-
// Grammar packages: tree-sitter-typescript, tree-sitter-python, etc.
|
|
61
|
-
// WASM file is at package root: node_modules/tree-sitter-{name}/tree-sitter-{name}.wasm
|
|
62
|
-
const packageName = `tree-sitter-${grammarName}`;
|
|
63
|
-
try {
|
|
64
|
-
// require.resolve returns bindings/node/index.js — walk up to package root
|
|
65
|
-
const pkgJson = require.resolve(`${packageName}/package.json`);
|
|
66
|
-
return resolve(dirname(pkgJson), `${packageName}.wasm`);
|
|
67
|
-
} catch {
|
|
68
|
-
// Fallback: try direct node_modules path
|
|
69
|
-
return resolve(process.cwd(), "node_modules", packageName, `${packageName}.wasm`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
1
|
+
import { Parser, Language, type Tree } from "web-tree-sitter";
|
|
2
|
+
import { resolve, dirname } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Singleton Tree-sitter engine with grammar caching.
|
|
6
|
+
* Parser.init() runs once; grammar WASMs are lazy-loaded and cached in-memory.
|
|
7
|
+
*/
|
|
8
|
+
export class TreeSitterEngine {
|
|
9
|
+
private static instance: TreeSitterEngine | null = null;
|
|
10
|
+
private parser: Parser | null = null;
|
|
11
|
+
private grammarCache = new Map<string, Language>();
|
|
12
|
+
private initPromise: Promise<void> | null = null;
|
|
13
|
+
|
|
14
|
+
static async getInstance(): Promise<TreeSitterEngine> {
|
|
15
|
+
if (!this.instance) {
|
|
16
|
+
this.instance = new TreeSitterEngine();
|
|
17
|
+
await this.instance.init();
|
|
18
|
+
}
|
|
19
|
+
return this.instance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Reset singleton — for testing only */
|
|
23
|
+
static resetForTest(): void {
|
|
24
|
+
if (this.instance?.parser) {
|
|
25
|
+
this.instance.parser.delete();
|
|
26
|
+
}
|
|
27
|
+
this.instance = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async init(): Promise<void> {
|
|
31
|
+
if (this.initPromise) return this.initPromise;
|
|
32
|
+
this.initPromise = (async () => {
|
|
33
|
+
await Parser.init();
|
|
34
|
+
this.parser = new Parser();
|
|
35
|
+
})();
|
|
36
|
+
return this.initPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async parse(source: string, grammarName: string): Promise<Tree> {
|
|
40
|
+
const grammar = await this.loadGrammar(grammarName);
|
|
41
|
+
this.parser!.setLanguage(grammar);
|
|
42
|
+
const tree = this.parser!.parse(source);
|
|
43
|
+
if (!tree) throw new Error(`Failed to parse with grammar: ${grammarName}`);
|
|
44
|
+
return tree;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async loadGrammar(grammarName: string): Promise<Language> {
|
|
48
|
+
const cached = this.grammarCache.get(grammarName);
|
|
49
|
+
if (cached) return cached;
|
|
50
|
+
|
|
51
|
+
const wasmPath = resolveGrammarWasm(grammarName);
|
|
52
|
+
const lang = await Language.load(wasmPath);
|
|
53
|
+
this.grammarCache.set(grammarName, lang);
|
|
54
|
+
return lang;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve WASM file path from installed npm grammar package */
|
|
59
|
+
function resolveGrammarWasm(grammarName: string): string {
|
|
60
|
+
// Grammar packages: tree-sitter-typescript, tree-sitter-python, etc.
|
|
61
|
+
// WASM file is at package root: node_modules/tree-sitter-{name}/tree-sitter-{name}.wasm
|
|
62
|
+
const packageName = `tree-sitter-${grammarName}`;
|
|
63
|
+
try {
|
|
64
|
+
// require.resolve returns bindings/node/index.js — walk up to package root
|
|
65
|
+
const pkgJson = require.resolve(`${packageName}/package.json`);
|
|
66
|
+
return resolve(dirname(pkgJson), `${packageName}.wasm`);
|
|
67
|
+
} catch {
|
|
68
|
+
// Fallback: try direct node_modules path
|
|
69
|
+
return resolve(process.cwd(), "node_modules", packageName, `${packageName}.wasm`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { HologramResult } from "./types";
|
|
2
|
-
|
|
3
|
-
/** L0 fallback: return full source when tree-sitter cannot parse */
|
|
4
|
-
export function fallbackL0(filePath: string, source: string): HologramResult {
|
|
5
|
-
return {
|
|
6
|
-
hologram: source,
|
|
7
|
-
originalLength: source.length,
|
|
8
|
-
hologramLength: source.length,
|
|
9
|
-
savings: 0,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
1
|
+
import type { HologramResult } from "./types";
|
|
2
|
+
|
|
3
|
+
/** L0 fallback: return full source when tree-sitter cannot parse */
|
|
4
|
+
export function fallbackL0(filePath: string, source: string): HologramResult {
|
|
5
|
+
return {
|
|
6
|
+
hologram: source,
|
|
7
|
+
originalLength: source.length,
|
|
8
|
+
hologramLength: source.length,
|
|
9
|
+
savings: 0,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Node, Tree } from "web-tree-sitter";
|
|
2
|
+
import type { LanguageExtractor, HologramOptions } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Collapse multiple whitespace/newlines into a single space.
|
|
6
|
+
*/
|
|
7
|
+
function collapse(s: string): string {
|
|
8
|
+
return s.replace(/\s+/g, " ").trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract the text of a field, collapsing whitespace.
|
|
13
|
+
*/
|
|
14
|
+
function fieldText(node: Node, field: string): string {
|
|
15
|
+
return collapse(node.childForFieldName(field)?.text ?? "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format a Go function/method signature with stubbed body.
|
|
20
|
+
* Slices source up to (but not including) the body block.
|
|
21
|
+
*/
|
|
22
|
+
function stubGoBody(node: Node, source: string): string {
|
|
23
|
+
const body = node.childForFieldName("body");
|
|
24
|
+
if (!body) return collapse(node.text);
|
|
25
|
+
return collapse(source.slice(node.startIndex, body.startIndex).trimEnd()) + " {…}";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract a `function_declaration`:
|
|
30
|
+
* func Name(params) ReturnType {…}
|
|
31
|
+
*/
|
|
32
|
+
function extractFunction(node: Node, source: string): string {
|
|
33
|
+
return stubGoBody(node, source);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract a `method_declaration`:
|
|
38
|
+
* func (r ReceiverType) Name(params) ReturnType {…}
|
|
39
|
+
*/
|
|
40
|
+
function extractMethod(node: Node, source: string): string {
|
|
41
|
+
return stubGoBody(node, source);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract a `type_spec` node (the individual name+type inside a type declaration).
|
|
46
|
+
* Handles struct, interface, and other type aliases/definitions.
|
|
47
|
+
*/
|
|
48
|
+
function extractTypeSpec(node: Node): string {
|
|
49
|
+
const name = fieldText(node, "name");
|
|
50
|
+
const typeNode = node.childForFieldName("type");
|
|
51
|
+
if (!typeNode) return `type ${name}`;
|
|
52
|
+
|
|
53
|
+
switch (typeNode.type) {
|
|
54
|
+
case "struct_type":
|
|
55
|
+
return extractStructType(name, typeNode);
|
|
56
|
+
case "interface_type":
|
|
57
|
+
return extractInterfaceType(name, typeNode);
|
|
58
|
+
default:
|
|
59
|
+
// type alias or other: e.g. `type MyInt int`, `type Handler func(...)`
|
|
60
|
+
return `type ${name} ${collapse(typeNode.text)}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract a struct type with all field declarations.
|
|
66
|
+
*
|
|
67
|
+
* Example output:
|
|
68
|
+
* type DaemonState struct {
|
|
69
|
+
* Running bool
|
|
70
|
+
* Pid int
|
|
71
|
+
* }
|
|
72
|
+
*/
|
|
73
|
+
function extractStructType(name: string, structNode: Node): string {
|
|
74
|
+
const fieldList = structNode.namedChildren.find(c => c.type === "field_declaration_list");
|
|
75
|
+
if (!fieldList || fieldList.namedChildCount === 0) {
|
|
76
|
+
return `type ${name} struct {}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fields: string[] = [];
|
|
80
|
+
for (const field of fieldList.namedChildren) {
|
|
81
|
+
if (field.type === "field_declaration") {
|
|
82
|
+
// Collect field names (may be multiple: `X, Y int`)
|
|
83
|
+
const names: string[] = [];
|
|
84
|
+
let typeNode: Node | null = null;
|
|
85
|
+
for (const child of field.namedChildren) {
|
|
86
|
+
if (child.type === "field_identifier" || child.type === "identifier") {
|
|
87
|
+
names.push(child.text);
|
|
88
|
+
} else {
|
|
89
|
+
typeNode = child;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const typeStr = typeNode ? collapse(typeNode.text) : "?";
|
|
93
|
+
if (names.length > 0) {
|
|
94
|
+
fields.push(` ${names.join(", ")} ${typeStr}`);
|
|
95
|
+
} else {
|
|
96
|
+
// Embedded field: just the type
|
|
97
|
+
fields.push(` ${typeStr}`);
|
|
98
|
+
}
|
|
99
|
+
} else if (field.type === "comment") {
|
|
100
|
+
// Skip comments inside struct
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `type ${name} struct {\n${fields.join("\n")}\n}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract an interface type with method/embedded type signatures.
|
|
109
|
+
*
|
|
110
|
+
* Example output:
|
|
111
|
+
* type Reader interface {
|
|
112
|
+
* Read(p []byte) (n int, err error)
|
|
113
|
+
* }
|
|
114
|
+
*/
|
|
115
|
+
function extractInterfaceType(name: string, ifaceNode: Node): string {
|
|
116
|
+
// In tree-sitter-go, method_elem nodes are direct named children of interface_type
|
|
117
|
+
// (there is no intermediate interface_body wrapper node)
|
|
118
|
+
const members: string[] = [];
|
|
119
|
+
for (const child of ifaceNode.namedChildren) {
|
|
120
|
+
switch (child.type) {
|
|
121
|
+
case "method_elem":
|
|
122
|
+
case "interface_type_name":
|
|
123
|
+
case "type_constraint":
|
|
124
|
+
case "type_elem":
|
|
125
|
+
members.push(" " + collapse(child.text));
|
|
126
|
+
break;
|
|
127
|
+
case "comment":
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
members.push(" " + collapse(child.text));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (members.length === 0) return `type ${name} interface {}`;
|
|
135
|
+
return `type ${name} interface {\n${members.join("\n")}\n}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract a `type_declaration` node.
|
|
140
|
+
* A single `type_declaration` may contain one or more `type_spec` nodes
|
|
141
|
+
* (the grouped `type (...)` form).
|
|
142
|
+
*/
|
|
143
|
+
function extractTypeDeclaration(node: Node): string[] {
|
|
144
|
+
const specs = node.namedChildren.filter(c => c.type === "type_spec");
|
|
145
|
+
return specs.map(extractTypeSpec);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extract an `import_declaration`.
|
|
150
|
+
* Handles both single-spec (`import "fmt"`) and grouped (`import (...)`).
|
|
151
|
+
*/
|
|
152
|
+
function extractImport(node: Node): string {
|
|
153
|
+
return collapse(node.text);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract a `package_clause`.
|
|
158
|
+
*/
|
|
159
|
+
function extractPackage(node: Node): string {
|
|
160
|
+
return collapse(node.text);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Process a single top-level declaration node.
|
|
165
|
+
* Returns one or more lines (type declarations may emit multiple lines).
|
|
166
|
+
*/
|
|
167
|
+
function extractTopLevel(node: Node, source: string): string[] {
|
|
168
|
+
switch (node.type) {
|
|
169
|
+
case "package_clause":
|
|
170
|
+
return [extractPackage(node)];
|
|
171
|
+
case "import_declaration":
|
|
172
|
+
return [extractImport(node)];
|
|
173
|
+
case "function_declaration":
|
|
174
|
+
return [extractFunction(node, source)];
|
|
175
|
+
case "method_declaration":
|
|
176
|
+
return [extractMethod(node, source)];
|
|
177
|
+
case "type_declaration":
|
|
178
|
+
return extractTypeDeclaration(node);
|
|
179
|
+
case "comment":
|
|
180
|
+
// Skip top-level comments (doc comments) for compression
|
|
181
|
+
return [];
|
|
182
|
+
default:
|
|
183
|
+
// Variables, constants, and other top-level declarations: skip bodies,
|
|
184
|
+
// emit a stub if meaningful.
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const goExtractor: LanguageExtractor = {
|
|
190
|
+
extensions: ["go"],
|
|
191
|
+
grammarName: "go",
|
|
192
|
+
|
|
193
|
+
extract(tree: Tree, source: string, _options?: HologramOptions): string[] {
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
|
|
196
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
197
|
+
const extracted = extractTopLevel(node, source);
|
|
198
|
+
lines.push(...extracted);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return lines;
|
|
202
|
+
},
|
|
203
|
+
};
|