gx-code-agent 0.0.0-stage → 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.
- package/README.md +276 -2
- package/bin/gx-stdio-mcp.js +145 -0
- package/bin/gx.js +99 -0
- package/contracts/README.md +40 -0
- package/contracts/alignment.schema.json +99 -0
- package/contracts/code-chunk.schema.json +28 -0
- package/contracts/evidence.schema.json +108 -0
- package/contracts/examples/alignment.example.json +26 -0
- package/contracts/examples/code-chunk.example.json +20 -0
- package/contracts/examples/evidence.add-field.json +49 -0
- package/contracts/examples/evidence.verify-manifest.json +52 -0
- package/contracts/examples/ext-method.example.json +14 -0
- package/contracts/examples/manifest.example.json +13 -0
- package/contracts/examples/unknown.extension-coverage.json +29 -0
- package/contracts/ext-method.schema.json +57 -0
- package/contracts/manifest.schema.json +27 -0
- package/contracts/unknown.schema.json +119 -0
- package/eval/README.md +52 -0
- package/eval/code-search-eval.json +216 -0
- package/package.json +37 -4
- package/src/adapters/cli/args.js +94 -0
- package/src/adapters/cli/output.js +66 -0
- package/src/adapters/platform/mcpClient.js +202 -0
- package/src/application/applyWorkflow.js +138 -0
- package/src/application/auditCommand.js +130 -0
- package/src/application/auditWorkflow.js +41 -0
- package/src/application/chunksCommand.js +87 -0
- package/src/application/errors.js +27 -0
- package/src/application/evalSearchCommand.js +148 -0
- package/src/application/initWorkflow.js +563 -0
- package/src/application/pullWorkflow.js +74 -0
- package/src/application/scanCommand.js +71 -0
- package/src/application/scanWorkflow.js +689 -0
- package/src/application/verifyWorkflow.js +142 -0
- package/src/domain/canonicalJson.js +72 -0
- package/src/domain/chunker.js +297 -0
- package/src/domain/digest.js +60 -0
- package/src/domain/java/JavaAstChunker.java +139 -0
- package/src/domain/java/JavaAstScanner.java +107 -0
- package/src/domain/javaAst.js +186 -0
- package/src/domain/jdk.js +66 -0
- package/src/domain/keepBlocks.js +122 -0
- package/src/store/backupStore.js +106 -0
- package/src/store/localStore.js +79 -0
- package/templates/platform-rules.md +29 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
package gx.tools;
|
|
2
|
+
|
|
3
|
+
import com.sun.source.tree.AnnotationTree;
|
|
4
|
+
import com.sun.source.tree.ClassTree;
|
|
5
|
+
import com.sun.source.tree.CompilationUnitTree;
|
|
6
|
+
import com.sun.source.tree.MethodTree;
|
|
7
|
+
import com.sun.source.tree.Tree;
|
|
8
|
+
import com.sun.source.util.JavacTask;
|
|
9
|
+
import com.sun.source.util.SourcePositions;
|
|
10
|
+
import com.sun.source.util.TreePathScanner;
|
|
11
|
+
import com.sun.source.util.Trees;
|
|
12
|
+
|
|
13
|
+
import javax.lang.model.element.Modifier;
|
|
14
|
+
import javax.tools.Diagnostic;
|
|
15
|
+
import javax.tools.DiagnosticCollector;
|
|
16
|
+
import javax.tools.JavaCompiler;
|
|
17
|
+
import javax.tools.JavaFileObject;
|
|
18
|
+
import javax.tools.StandardJavaFileManager;
|
|
19
|
+
import javax.tools.ToolProvider;
|
|
20
|
+
import java.io.BufferedReader;
|
|
21
|
+
import java.io.BufferedWriter;
|
|
22
|
+
import java.io.File;
|
|
23
|
+
import java.io.InputStreamReader;
|
|
24
|
+
import java.io.OutputStreamWriter;
|
|
25
|
+
import java.io.PrintWriter;
|
|
26
|
+
import java.nio.charset.StandardCharsets;
|
|
27
|
+
import java.util.ArrayList;
|
|
28
|
+
import java.util.List;
|
|
29
|
+
import java.util.Locale;
|
|
30
|
+
|
|
31
|
+
public final class JavaAstChunker {
|
|
32
|
+
|
|
33
|
+
private JavaAstChunker() {
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static void main(String[] args) throws Exception {
|
|
37
|
+
List<File> files = new ArrayList<>();
|
|
38
|
+
try (BufferedReader reader = new BufferedReader(
|
|
39
|
+
new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
|
|
40
|
+
String line;
|
|
41
|
+
while ((line = reader.readLine()) != null) {
|
|
42
|
+
if (!line.isBlank()) {
|
|
43
|
+
files.add(new File(line.trim()));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (files.isEmpty()) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
|
51
|
+
if (compiler == null) {
|
|
52
|
+
System.err.println("NO_JAVAC");
|
|
53
|
+
System.exit(2);
|
|
54
|
+
}
|
|
55
|
+
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
|
56
|
+
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null,
|
|
57
|
+
StandardCharsets.UTF_8);
|
|
58
|
+
JavacTask task = (JavacTask) compiler.getTask(null, fileManager, diagnostics, List.of("-proc:none"),
|
|
59
|
+
null, fileManager.getJavaFileObjectsFromFiles(files));
|
|
60
|
+
Iterable<? extends CompilationUnitTree> units = task.parse();
|
|
61
|
+
Trees trees = Trees.instance(task);
|
|
62
|
+
SourcePositions positions = trees.getSourcePositions();
|
|
63
|
+
PrintWriter out = new PrintWriter(new BufferedWriter(
|
|
64
|
+
new OutputStreamWriter(System.out, StandardCharsets.UTF_8)));
|
|
65
|
+
for (CompilationUnitTree unit : units) {
|
|
66
|
+
new TreePathScanner<Void, Void>() {
|
|
67
|
+
private final List<String> classes = new ArrayList<>();
|
|
68
|
+
|
|
69
|
+
@Override
|
|
70
|
+
public Void visitClass(ClassTree node, Void unused) {
|
|
71
|
+
String className = String.join(".", classes);
|
|
72
|
+
classes.add(node.getSimpleName().toString());
|
|
73
|
+
String fullName = String.join(".", classes);
|
|
74
|
+
long start = line(unit, positions.getStartPosition(unit, node));
|
|
75
|
+
long end = line(unit, positions.getEndPosition(unit, node));
|
|
76
|
+
if (start > 0 && end >= start) {
|
|
77
|
+
out.println("class\t" + sanitize(unit.getSourceFile().getName()) + "\t"
|
|
78
|
+
+ sanitize(fullName) + "\t\t" + start + "\t" + end + "\tfalse\t"
|
|
79
|
+
+ sanitize(annotations(node)));
|
|
80
|
+
}
|
|
81
|
+
super.visitClass(node, unused);
|
|
82
|
+
classes.remove(classes.size() - 1);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Override
|
|
87
|
+
public Void visitMethod(MethodTree node, Void unused) {
|
|
88
|
+
String className = String.join(".", classes);
|
|
89
|
+
Tree returnType = node.getReturnType();
|
|
90
|
+
long start = returnType == null ? positions.getStartPosition(unit, node)
|
|
91
|
+
: positions.getStartPosition(unit, returnType);
|
|
92
|
+
long end = positions.getEndPosition(unit, node);
|
|
93
|
+
long startLine = line(unit, start);
|
|
94
|
+
long endLine = line(unit, end);
|
|
95
|
+
if (startLine > 0 && endLine >= startLine) {
|
|
96
|
+
boolean isPublic = node.getModifiers().getFlags().contains(Modifier.PUBLIC);
|
|
97
|
+
out.println("method\t" + sanitize(unit.getSourceFile().getName()) + "\t"
|
|
98
|
+
+ sanitize(className) + "\t" + sanitize(node.getName().toString()) + "\t"
|
|
99
|
+
+ startLine + "\t" + endLine + "\t" + isPublic + "\t"
|
|
100
|
+
+ sanitize(annotations(node)));
|
|
101
|
+
}
|
|
102
|
+
return super.visitMethod(node, unused);
|
|
103
|
+
}
|
|
104
|
+
}.scan(unit, null);
|
|
105
|
+
}
|
|
106
|
+
out.flush();
|
|
107
|
+
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
|
|
108
|
+
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
|
|
109
|
+
String source = diagnostic.getSource() == null ? "" : diagnostic.getSource().getName();
|
|
110
|
+
System.err.println(source + ":" + diagnostic.getLineNumber() + ": "
|
|
111
|
+
+ diagnostic.getMessage(Locale.ROOT));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private static long line(CompilationUnitTree unit, long position) {
|
|
117
|
+
return position < 0 ? -1 : unit.getLineMap().getLineNumber(position);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private static String annotations(MethodTree node) {
|
|
121
|
+
List<String> values = new ArrayList<>();
|
|
122
|
+
for (AnnotationTree annotation : node.getModifiers().getAnnotations()) {
|
|
123
|
+
values.add(annotation.getAnnotationType().toString());
|
|
124
|
+
}
|
|
125
|
+
return String.join(",", values);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private static String annotations(ClassTree node) {
|
|
129
|
+
List<String> values = new ArrayList<>();
|
|
130
|
+
for (AnnotationTree annotation : node.getModifiers().getAnnotations()) {
|
|
131
|
+
values.add(annotation.getAnnotationType().toString());
|
|
132
|
+
}
|
|
133
|
+
return String.join(",", values);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private static String sanitize(String value) {
|
|
137
|
+
return value.replace('\t', ' ').replace('\n', ' ').replace('\r', ' ');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package gx.tools;
|
|
2
|
+
|
|
3
|
+
import com.sun.source.tree.AnnotationTree;
|
|
4
|
+
import com.sun.source.tree.ClassTree;
|
|
5
|
+
import com.sun.source.tree.CompilationUnitTree;
|
|
6
|
+
import com.sun.source.tree.MethodTree;
|
|
7
|
+
import com.sun.source.util.JavacTask;
|
|
8
|
+
import com.sun.source.util.SourcePositions;
|
|
9
|
+
import com.sun.source.util.TreePathScanner;
|
|
10
|
+
import com.sun.source.util.Trees;
|
|
11
|
+
|
|
12
|
+
import javax.lang.model.element.Modifier;
|
|
13
|
+
import javax.tools.Diagnostic;
|
|
14
|
+
import javax.tools.DiagnosticCollector;
|
|
15
|
+
import javax.tools.JavaCompiler;
|
|
16
|
+
import javax.tools.JavaFileObject;
|
|
17
|
+
import javax.tools.StandardJavaFileManager;
|
|
18
|
+
import javax.tools.ToolProvider;
|
|
19
|
+
import java.io.BufferedReader;
|
|
20
|
+
import java.io.BufferedWriter;
|
|
21
|
+
import java.io.File;
|
|
22
|
+
import java.io.InputStreamReader;
|
|
23
|
+
import java.io.OutputStreamWriter;
|
|
24
|
+
import java.io.PrintWriter;
|
|
25
|
+
import java.nio.charset.StandardCharsets;
|
|
26
|
+
import java.util.ArrayList;
|
|
27
|
+
import java.util.List;
|
|
28
|
+
import java.util.Locale;
|
|
29
|
+
|
|
30
|
+
public final class JavaAstScanner {
|
|
31
|
+
|
|
32
|
+
private JavaAstScanner() {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static void main(String[] args) throws Exception {
|
|
36
|
+
List<File> files = new ArrayList<>();
|
|
37
|
+
try (BufferedReader reader = new BufferedReader(
|
|
38
|
+
new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
|
|
39
|
+
String line;
|
|
40
|
+
while ((line = reader.readLine()) != null) {
|
|
41
|
+
if (!line.isBlank()) {
|
|
42
|
+
files.add(new File(line.trim()));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (files.isEmpty()) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
|
50
|
+
if (compiler == null) {
|
|
51
|
+
System.err.println("NO_JAVAC");
|
|
52
|
+
System.exit(2);
|
|
53
|
+
}
|
|
54
|
+
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
|
55
|
+
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null,
|
|
56
|
+
StandardCharsets.UTF_8);
|
|
57
|
+
JavacTask task = (JavacTask) compiler.getTask(null, fileManager, diagnostics, List.of("-proc:none"),
|
|
58
|
+
null, fileManager.getJavaFileObjectsFromFiles(files));
|
|
59
|
+
Iterable<? extends CompilationUnitTree> units = task.parse();
|
|
60
|
+
Trees trees = Trees.instance(task);
|
|
61
|
+
SourcePositions positions = trees.getSourcePositions();
|
|
62
|
+
PrintWriter out = new PrintWriter(new BufferedWriter(
|
|
63
|
+
new OutputStreamWriter(System.out, StandardCharsets.UTF_8)));
|
|
64
|
+
for (CompilationUnitTree unit : units) {
|
|
65
|
+
new TreePathScanner<Void, Void>() {
|
|
66
|
+
private final List<String> classes = new ArrayList<>();
|
|
67
|
+
|
|
68
|
+
@Override
|
|
69
|
+
public Void visitClass(ClassTree node, Void unused) {
|
|
70
|
+
classes.add(node.getSimpleName().toString());
|
|
71
|
+
super.visitClass(node, unused);
|
|
72
|
+
classes.remove(classes.size() - 1);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Override
|
|
77
|
+
public Void visitMethod(MethodTree node, Void unused) {
|
|
78
|
+
if (node.getModifiers().getFlags().contains(Modifier.PUBLIC)) {
|
|
79
|
+
long position = positions.getStartPosition(unit, node);
|
|
80
|
+
long line = position < 0 ? 0 : unit.getLineMap().getLineNumber(position);
|
|
81
|
+
List<String> annotations = new ArrayList<>();
|
|
82
|
+
for (AnnotationTree annotation : node.getModifiers().getAnnotations()) {
|
|
83
|
+
annotations.add(annotation.getAnnotationType().toString());
|
|
84
|
+
}
|
|
85
|
+
out.println(sanitize(unit.getSourceFile().getName()) + "\t"
|
|
86
|
+
+ sanitize(String.join(".", classes)) + "\t"
|
|
87
|
+
+ sanitize(node.getName().toString()) + "\t" + line + "\t"
|
|
88
|
+
+ sanitize(String.join(",", annotations)));
|
|
89
|
+
}
|
|
90
|
+
return super.visitMethod(node, unused);
|
|
91
|
+
}
|
|
92
|
+
}.scan(unit, null);
|
|
93
|
+
}
|
|
94
|
+
out.flush();
|
|
95
|
+
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
|
|
96
|
+
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
|
|
97
|
+
String source = diagnostic.getSource() == null ? "" : diagnostic.getSource().getName();
|
|
98
|
+
System.err.println(source + ":" + diagnostic.getLineNumber() + ": "
|
|
99
|
+
+ diagnostic.getMessage(Locale.ROOT));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static String sanitize(String value) {
|
|
105
|
+
return value.replace('\t', ' ').replace('\n', ' ').replace('\r', ' ');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { resolveJdkTools } from "./jdk.js";
|
|
9
|
+
|
|
10
|
+
const JAVA_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "java");
|
|
11
|
+
const SCANNER_SOURCE = path.join(JAVA_DIR, "JavaAstScanner.java");
|
|
12
|
+
const CHUNKER_SOURCE = path.join(JAVA_DIR, "JavaAstChunker.java");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 编译并缓存一个 Java AST 工具。
|
|
16
|
+
*
|
|
17
|
+
* @param {string} sourcePath
|
|
18
|
+
* @param {{ javac: string, major: number }} tools
|
|
19
|
+
* @param {string} className
|
|
20
|
+
* @returns {{ ok: boolean, cacheDir?: string, reason?: string }}
|
|
21
|
+
*/
|
|
22
|
+
function ensureTool(sourcePath, tools, className) {
|
|
23
|
+
if (!fs.existsSync(sourcePath)) {
|
|
24
|
+
return { ok: false, reason: `AST 工具源码不存在:${sourcePath}` };
|
|
25
|
+
}
|
|
26
|
+
const source = fs.readFileSync(sourcePath, "utf8");
|
|
27
|
+
const cacheDir = path.join(os.tmpdir(), "gx-java-ast",
|
|
28
|
+
`${createHash("sha256").update(source).digest("hex").slice(0, 16)}-${tools.major}-${className}`);
|
|
29
|
+
const classFile = path.join(cacheDir, ...className.split(".")) + ".class";
|
|
30
|
+
if (fs.existsSync(classFile)) {
|
|
31
|
+
return { ok: true, cacheDir };
|
|
32
|
+
}
|
|
33
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
34
|
+
const compiled = spawnSync(tools.javac, ["-encoding", "UTF-8", "-d", cacheDir, sourcePath], {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
maxBuffer: 10 * 1024 * 1024
|
|
37
|
+
});
|
|
38
|
+
if (compiled.error) {
|
|
39
|
+
return { ok: false, reason: `AST 工具编译失败:${compiled.error.message}` };
|
|
40
|
+
}
|
|
41
|
+
if (compiled.status !== 0) {
|
|
42
|
+
const detail = `${compiled.stdout ?? ""}\n${compiled.stderr ?? ""}`.trim().split(/\r?\n/u)[0];
|
|
43
|
+
return { ok: false, reason: `AST 工具编译失败:${detail}` };
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, cacheDir };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 执行 Java AST 工具。
|
|
50
|
+
*
|
|
51
|
+
* @param {{ java: string }} tools
|
|
52
|
+
* @param {string} className
|
|
53
|
+
* @param {string} cacheDir
|
|
54
|
+
* @param {string[]} files
|
|
55
|
+
* @returns {object}
|
|
56
|
+
*/
|
|
57
|
+
function runTool(tools, className, cacheDir, files) {
|
|
58
|
+
return spawnSync(tools.java, ["-cp", cacheDir, className], {
|
|
59
|
+
input: files.join("\n"),
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
maxBuffer: 20 * 1024 * 1024
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 用 JDK Compiler Tree API 解析 Java 方法、注解与行号。
|
|
67
|
+
*
|
|
68
|
+
* @param {{ files: string[] }} input
|
|
69
|
+
* @returns {{ available: boolean, methods: object[], warnings: string[], reason: string | null }}
|
|
70
|
+
*/
|
|
71
|
+
export function scanJavaAst({ files }) {
|
|
72
|
+
if (!files || files.length === 0) {
|
|
73
|
+
return { available: true, methods: [], warnings: [], reason: null };
|
|
74
|
+
}
|
|
75
|
+
const tools = resolveJdkTools();
|
|
76
|
+
if (!tools.ok) {
|
|
77
|
+
return { available: false, methods: [], warnings: [], reason: tools.reason };
|
|
78
|
+
}
|
|
79
|
+
const scanner = ensureTool(SCANNER_SOURCE, tools, "gx.tools.JavaAstScanner");
|
|
80
|
+
if (!scanner.ok) {
|
|
81
|
+
return { available: false, methods: [], warnings: [], reason: scanner.reason };
|
|
82
|
+
}
|
|
83
|
+
const result = runTool(tools, "gx.tools.JavaAstScanner", scanner.cacheDir, files);
|
|
84
|
+
if (result.error) {
|
|
85
|
+
return { available: false, methods: [], warnings: [], reason: `AST 扫描执行失败:${result.error.message}` };
|
|
86
|
+
}
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
const detail = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim().split(/\r?\n/u)[0];
|
|
89
|
+
return { available: false, methods: [], warnings: [], reason: `AST 扫描执行失败:${detail}` };
|
|
90
|
+
}
|
|
91
|
+
const methods = [];
|
|
92
|
+
for (const line of (result.stdout ?? "").split(/\r?\n/u)) {
|
|
93
|
+
if (!line) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const [file, className, methodName, lineText, annotationText] = line.split("\t");
|
|
97
|
+
if (!file || !className || !methodName) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
methods.push({
|
|
101
|
+
file: path.resolve(file),
|
|
102
|
+
className,
|
|
103
|
+
methodName,
|
|
104
|
+
line: Number.parseInt(lineText, 10) || 0,
|
|
105
|
+
annotations: annotationText ? annotationText.split(",").filter(Boolean) : []
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
available: true,
|
|
110
|
+
methods,
|
|
111
|
+
warnings: warnings(result.stderr),
|
|
112
|
+
reason: null
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 用 JDK Compiler Tree API 生成 Java 类/方法切片边界。
|
|
118
|
+
*
|
|
119
|
+
* @param {{ files: string[] }} input
|
|
120
|
+
* @returns {{ available: boolean, chunks: object[], warnings: string[], reason: string | null }}
|
|
121
|
+
*/
|
|
122
|
+
export function scanJavaChunks({ files }) {
|
|
123
|
+
if (!files || files.length === 0) {
|
|
124
|
+
return { available: true, chunks: [], warnings: [], reason: null };
|
|
125
|
+
}
|
|
126
|
+
const tools = resolveJdkTools();
|
|
127
|
+
if (!tools.ok) {
|
|
128
|
+
return { available: false, chunks: [], warnings: [], reason: tools.reason };
|
|
129
|
+
}
|
|
130
|
+
const chunker = ensureTool(CHUNKER_SOURCE, tools, "gx.tools.JavaAstChunker");
|
|
131
|
+
if (!chunker.ok) {
|
|
132
|
+
return { available: false, chunks: [], warnings: [], reason: chunker.reason };
|
|
133
|
+
}
|
|
134
|
+
const result = runTool(tools, "gx.tools.JavaAstChunker", chunker.cacheDir, files);
|
|
135
|
+
if (result.error) {
|
|
136
|
+
return { available: false, chunks: [], warnings: [], reason: `AST 切片执行失败:${result.error.message}` };
|
|
137
|
+
}
|
|
138
|
+
if (result.status !== 0) {
|
|
139
|
+
const detail = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim().split(/\r?\n/u)[0];
|
|
140
|
+
return { available: false, chunks: [], warnings: [], reason: `AST 切片执行失败:${detail}` };
|
|
141
|
+
}
|
|
142
|
+
const chunks = [];
|
|
143
|
+
for (const line of (result.stdout ?? "").split(/\r?\n/u)) {
|
|
144
|
+
if (!line) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const [kind, file, className, methodName, startText, endText, publicText, annotationText] = line.split("\t");
|
|
148
|
+
if (!kind || !file || !className) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const startLine = Number.parseInt(startText, 10) || 0;
|
|
152
|
+
const endLine = Number.parseInt(endText, 10) || 0;
|
|
153
|
+
if (startLine < 1 || endLine < startLine) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
chunks.push({
|
|
157
|
+
kind,
|
|
158
|
+
file: path.resolve(file),
|
|
159
|
+
className,
|
|
160
|
+
methodName: methodName || null,
|
|
161
|
+
startLine,
|
|
162
|
+
endLine,
|
|
163
|
+
isPublic: publicText === "true",
|
|
164
|
+
annotations: annotationText ? annotationText.split(",").filter(Boolean) : []
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
available: true,
|
|
169
|
+
chunks,
|
|
170
|
+
warnings: warnings(result.stderr),
|
|
171
|
+
reason: null
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 提取工具 stderr 中的诊断信息。
|
|
177
|
+
*
|
|
178
|
+
* @param {string | null} text
|
|
179
|
+
* @returns {string[]}
|
|
180
|
+
*/
|
|
181
|
+
function warnings(text) {
|
|
182
|
+
return (text ?? "").split(/\r?\n/u)
|
|
183
|
+
.map((line) => line.trim())
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.slice(0, 20);
|
|
186
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
let cached;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 解析 javac 主版本。
|
|
9
|
+
*
|
|
10
|
+
* @param {string} javac
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
function javacMajor(javac) {
|
|
14
|
+
const result = spawnSync(javac, ["-version"], { encoding: "utf8" });
|
|
15
|
+
const text = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
16
|
+
const match = text.match(/javac\s+(?:1\.)?(\d+)/u);
|
|
17
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 选择可用的 JDK 9+(AST Tree API 与 JDK 17 class 文件需要)。
|
|
22
|
+
*
|
|
23
|
+
* @returns {{ ok: boolean, javac?: string, java?: string, reason?: string }}
|
|
24
|
+
*/
|
|
25
|
+
export function resolveJdkTools() {
|
|
26
|
+
if (cached) {
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
const candidates = [];
|
|
30
|
+
if (process.env.JAVAC) {
|
|
31
|
+
candidates.push({
|
|
32
|
+
javac: process.env.JAVAC,
|
|
33
|
+
java: process.env.JAVA || (process.env.JAVA_HOME
|
|
34
|
+
? path.join(process.env.JAVA_HOME, "bin", "java") : "java")
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (process.env.JAVA_HOME) {
|
|
38
|
+
candidates.push({
|
|
39
|
+
javac: path.join(process.env.JAVA_HOME, "bin", "javac"),
|
|
40
|
+
java: path.join(process.env.JAVA_HOME, "bin", "java")
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
candidates.push({ javac: "javac", java: "java" });
|
|
44
|
+
if (process.platform === "darwin") {
|
|
45
|
+
for (const version of ["17", "21", "23"]) {
|
|
46
|
+
const home = spawnSync("/usr/libexec/java_home", ["-v", version], { encoding: "utf8" });
|
|
47
|
+
if (home.status === 0 && home.stdout.trim()) {
|
|
48
|
+
candidates.push({
|
|
49
|
+
javac: path.join(home.stdout.trim(), "bin", "javac"),
|
|
50
|
+
java: path.join(home.stdout.trim(), "bin", "java")
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (fs.existsSync(candidate.javac) || candidate.javac === "javac") {
|
|
57
|
+
const major = javacMajor(candidate.javac);
|
|
58
|
+
if (major >= 9) {
|
|
59
|
+
cached = { ok: true, ...candidate, major };
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
cached = { ok: false, reason: "需要 JDK 9+;请设置 JAVA_HOME 或 JAVAC" };
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GX 生成文件的保留块合并。
|
|
3
|
+
*
|
|
4
|
+
* 生成文件中的 `[CN01-xxx] start ... end-xxx` 块归用户/AI 所有;
|
|
5
|
+
* 重新生成时必须保留目标文件里的块内容。
|
|
6
|
+
*
|
|
7
|
+
* @param {string} existing 现有文件内容
|
|
8
|
+
* @param {string} generated 新生成文件内容
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function mergeKeepBlocks(existing, generated) {
|
|
12
|
+
if (!existing) {
|
|
13
|
+
return generated;
|
|
14
|
+
}
|
|
15
|
+
const generatedLines = generated.split("\n");
|
|
16
|
+
const existingBlocks = collectBlocks(existing);
|
|
17
|
+
|
|
18
|
+
const output = [];
|
|
19
|
+
for (let i = 0; i < generatedLines.length; i += 1) {
|
|
20
|
+
const line = generatedLines[i];
|
|
21
|
+
const name = startBlockName(line);
|
|
22
|
+
if (name === null) {
|
|
23
|
+
output.push(line);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const endIndex = findEnd(generatedLines, i + 1, name);
|
|
27
|
+
if (endIndex < 0) {
|
|
28
|
+
output.push(line);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
output.push(line);
|
|
32
|
+
const oldBlock = existingBlocks.get(name);
|
|
33
|
+
if (oldBlock && oldBlock.length > 0) {
|
|
34
|
+
output.push(...oldBlock);
|
|
35
|
+
} else {
|
|
36
|
+
output.push(...generatedLines.slice(i + 1, endIndex));
|
|
37
|
+
}
|
|
38
|
+
output.push(generatedLines[endIndex]);
|
|
39
|
+
i = endIndex;
|
|
40
|
+
}
|
|
41
|
+
return output.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 提取 start 行中的块名。
|
|
46
|
+
*
|
|
47
|
+
* @param {string} line
|
|
48
|
+
* @returns {string | null}
|
|
49
|
+
*/
|
|
50
|
+
export function startBlockName(line) {
|
|
51
|
+
const match = line.match(/\[CN01-([^\]]+)\]\s*start/u);
|
|
52
|
+
return match ? match[1] : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 查找结束行。
|
|
57
|
+
*
|
|
58
|
+
* @param {string[]} lines
|
|
59
|
+
* @param {number} from
|
|
60
|
+
* @param {string} name
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
function findEnd(lines, from, name) {
|
|
64
|
+
for (let i = from; i < lines.length; i += 1) {
|
|
65
|
+
if (lines[i].includes(`end-${name}`)) {
|
|
66
|
+
return i;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 收集现有文件的块内容(不含 start/end 行)。
|
|
74
|
+
*
|
|
75
|
+
* @param {string} content
|
|
76
|
+
* @returns {Map<string, string[]>}
|
|
77
|
+
*/
|
|
78
|
+
export function collectBlocks(content) {
|
|
79
|
+
const lines = content.split("\n");
|
|
80
|
+
const blocks = new Map();
|
|
81
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
82
|
+
const name = startBlockName(lines[i]);
|
|
83
|
+
if (name === null) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const endIndex = findEnd(lines, i + 1, name);
|
|
87
|
+
if (endIndex < 0) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
blocks.set(name, lines.slice(i + 1, endIndex));
|
|
91
|
+
i = endIndex;
|
|
92
|
+
}
|
|
93
|
+
return blocks;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 去掉保留块内部内容,只保留 start/end 行,用于漂移校验。
|
|
97
|
+
*
|
|
98
|
+
* 生成层指纹只覆盖骨架,自定义块内容归用户所有,不参与漂移判断。
|
|
99
|
+
*
|
|
100
|
+
* @param {string} content
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export function stripKeepBlockContents(content) {
|
|
104
|
+
const lines = content.split("\n");
|
|
105
|
+
const output = [];
|
|
106
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
107
|
+
const name = startBlockName(lines[i]);
|
|
108
|
+
if (name === null) {
|
|
109
|
+
output.push(lines[i]);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const endIndex = findEnd(lines, i + 1, name);
|
|
113
|
+
if (endIndex < 0) {
|
|
114
|
+
output.push(lines[i]);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
output.push(lines[i]);
|
|
118
|
+
output.push(lines[endIndex]);
|
|
119
|
+
i = endIndex;
|
|
120
|
+
}
|
|
121
|
+
return output.join("\n");
|
|
122
|
+
}
|