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.
Files changed (45) hide show
  1. package/README.md +276 -2
  2. package/bin/gx-stdio-mcp.js +145 -0
  3. package/bin/gx.js +99 -0
  4. package/contracts/README.md +40 -0
  5. package/contracts/alignment.schema.json +99 -0
  6. package/contracts/code-chunk.schema.json +28 -0
  7. package/contracts/evidence.schema.json +108 -0
  8. package/contracts/examples/alignment.example.json +26 -0
  9. package/contracts/examples/code-chunk.example.json +20 -0
  10. package/contracts/examples/evidence.add-field.json +49 -0
  11. package/contracts/examples/evidence.verify-manifest.json +52 -0
  12. package/contracts/examples/ext-method.example.json +14 -0
  13. package/contracts/examples/manifest.example.json +13 -0
  14. package/contracts/examples/unknown.extension-coverage.json +29 -0
  15. package/contracts/ext-method.schema.json +57 -0
  16. package/contracts/manifest.schema.json +27 -0
  17. package/contracts/unknown.schema.json +119 -0
  18. package/eval/README.md +52 -0
  19. package/eval/code-search-eval.json +216 -0
  20. package/package.json +37 -4
  21. package/src/adapters/cli/args.js +94 -0
  22. package/src/adapters/cli/output.js +66 -0
  23. package/src/adapters/platform/mcpClient.js +202 -0
  24. package/src/application/applyWorkflow.js +138 -0
  25. package/src/application/auditCommand.js +130 -0
  26. package/src/application/auditWorkflow.js +41 -0
  27. package/src/application/chunksCommand.js +87 -0
  28. package/src/application/errors.js +27 -0
  29. package/src/application/evalSearchCommand.js +148 -0
  30. package/src/application/initWorkflow.js +563 -0
  31. package/src/application/pullWorkflow.js +74 -0
  32. package/src/application/scanCommand.js +71 -0
  33. package/src/application/scanWorkflow.js +689 -0
  34. package/src/application/verifyWorkflow.js +142 -0
  35. package/src/domain/canonicalJson.js +72 -0
  36. package/src/domain/chunker.js +297 -0
  37. package/src/domain/digest.js +60 -0
  38. package/src/domain/java/JavaAstChunker.java +139 -0
  39. package/src/domain/java/JavaAstScanner.java +107 -0
  40. package/src/domain/javaAst.js +186 -0
  41. package/src/domain/jdk.js +66 -0
  42. package/src/domain/keepBlocks.js +122 -0
  43. package/src/store/backupStore.js +106 -0
  44. package/src/store/localStore.js +79 -0
  45. 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
+ }