@cassiomc1/forgeloop 1.12.0 → 1.13.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/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +13 -11
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/GUIDE_ROUTER.md +418 -9
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +44 -33
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/completions/_forgeloop +3 -3
- package/completions/forgeloop.bash +3 -3
- package/completions/forgeloop.fish +7 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
- package/docs/CLI_REFERENCE.md +28 -6
- package/docs/DOCUMENTATION_GUIDE.md +2 -1
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/PACKAGE_CONTENTS.md +28 -14
- package/docs/RECIPES.md +23 -0
- package/docs/RELEASE_CHECKLIST.md +30 -2
- package/docs/TROUBLESHOOTING.md +100 -2
- package/docs/documentation-manifest.json +652 -0
- package/docs/protocol-requirements.json +77 -0
- package/package.json +19 -4
- package/schemas/routing-input.schema.json +1 -1
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/generate-agent-protocol-summary.mjs +36 -0
- package/src/commands/next.js +19 -7
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/config/guides.json +44 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/cli-command-definitions.js +8 -1
- package/src/core/command-executors.js +5 -3
- package/src/core/command-input.js +140 -102
- package/src/core/contract-presets.js +82 -0
- package/src/core/error-codes.js +3 -3
- package/src/core/filesystem.js +1 -10
- package/src/core/go-project.js +206 -0
- package/src/core/java-project.js +403 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-explanation.js +63 -0
- package/src/core/php-project.js +85 -0
- package/src/core/project-detection.js +1760 -52
- package/src/core/reconcile-closure.js +4 -1
- package/src/core/router.js +156 -3
- package/src/core/rust-project.js +400 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { createLanguageProject, JAVA_SOURCE_EXTENSIONS, pathBelongsToRoot, portablePath } from "./multi-language-project.js";
|
|
2
|
+
import { maskBuildScript } from "./build-script.js";
|
|
3
|
+
import { parseXmlStructure } from "./xml-structure.js";
|
|
4
|
+
|
|
5
|
+
function invalidJava() {
|
|
6
|
+
return {
|
|
7
|
+
valid: false,
|
|
8
|
+
javaPlugin: false,
|
|
9
|
+
javaPlatform: false,
|
|
10
|
+
javaCompiler: false,
|
|
11
|
+
packaging: null,
|
|
12
|
+
modules: [],
|
|
13
|
+
includes: [],
|
|
14
|
+
gradleBuild: false,
|
|
15
|
+
gradleSettings: false,
|
|
16
|
+
gradleProperties: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function childNodes(node, name) {
|
|
21
|
+
return node.children.filter((child) => child.name.toLowerCase() === name.toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nodesAtPath(root, names) {
|
|
25
|
+
return names.reduce((nodes, name) => nodes.flatMap((node) => childNodes(node, name)), [root]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nodeValues(nodes) {
|
|
29
|
+
return nodes
|
|
30
|
+
.filter((node) => node.children.length === 0 && node.text.length <= 256)
|
|
31
|
+
.map((node) => node.text.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseMavenPom(text) {
|
|
36
|
+
const document = parseXmlStructure(text);
|
|
37
|
+
if (!document || document.root.name.toLowerCase() !== "project") return invalidJava();
|
|
38
|
+
const root = document.root;
|
|
39
|
+
const packaging = nodeValues(childNodes(root, "packaging"))[0] ?? null;
|
|
40
|
+
const modules = nodeValues(nodesAtPath(root, ["modules", "module"]));
|
|
41
|
+
const compilerProperties = ["release", "source", "target"]
|
|
42
|
+
.some((name) => nodeValues(nodesAtPath(root, ["properties", `maven.compiler.${name}`])).length > 0);
|
|
43
|
+
const compilerPlugin = [
|
|
44
|
+
["build", "plugins", "plugin", "artifactId"],
|
|
45
|
+
["build", "pluginManagement", "plugins", "plugin", "artifactId"],
|
|
46
|
+
].some((path) => nodeValues(nodesAtPath(root, path)).some((value) => value === "maven-compiler-plugin"));
|
|
47
|
+
const javaCompiler = compilerProperties || compilerPlugin;
|
|
48
|
+
const valid = nodeValues(childNodes(root, "artifactId")).length > 0
|
|
49
|
+
|| packaging === "pom"
|
|
50
|
+
|| modules.length > 0
|
|
51
|
+
|| javaCompiler;
|
|
52
|
+
return { valid, javaPlugin: false, javaPlatform: false, javaCompiler, packaging, modules };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function maskGradleStringsAndComments(text) {
|
|
56
|
+
let result = "";
|
|
57
|
+
let state = "code";
|
|
58
|
+
let quote = null;
|
|
59
|
+
let escaped = false;
|
|
60
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
61
|
+
const character = text[index];
|
|
62
|
+
const next = text[index + 1];
|
|
63
|
+
if (state === "string") {
|
|
64
|
+
if (escaped) escaped = false;
|
|
65
|
+
else if (character === "\\") escaped = true;
|
|
66
|
+
else if (character === quote) {
|
|
67
|
+
state = "code";
|
|
68
|
+
quote = null;
|
|
69
|
+
}
|
|
70
|
+
result += character === "\n" || character === "\r" ? character : " ";
|
|
71
|
+
} else if (state === "line-comment") {
|
|
72
|
+
if (character === "\n" || character === "\r") {
|
|
73
|
+
result += character;
|
|
74
|
+
state = "code";
|
|
75
|
+
} else result += " ";
|
|
76
|
+
} else if (state === "block-comment") {
|
|
77
|
+
if (character === "*" && next === "/") {
|
|
78
|
+
result += " ";
|
|
79
|
+
index += 1;
|
|
80
|
+
state = "code";
|
|
81
|
+
} else result += character === "\n" || character === "\r" ? character : " ";
|
|
82
|
+
} else if (character === "\"" || character === "'") {
|
|
83
|
+
result += " ";
|
|
84
|
+
quote = character;
|
|
85
|
+
state = "string";
|
|
86
|
+
} else if (character === "/" && next === "/") {
|
|
87
|
+
result += " ";
|
|
88
|
+
index += 1;
|
|
89
|
+
state = "line-comment";
|
|
90
|
+
} else if (character === "/" && next === "*") {
|
|
91
|
+
result += " ";
|
|
92
|
+
index += 1;
|
|
93
|
+
state = "block-comment";
|
|
94
|
+
} else {
|
|
95
|
+
result += character;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return state === "code" ? result : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function stripGradleComments(text) {
|
|
102
|
+
let result = "";
|
|
103
|
+
let state = "code";
|
|
104
|
+
let quote = null;
|
|
105
|
+
let escaped = false;
|
|
106
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
107
|
+
const character = text[index];
|
|
108
|
+
const next = text[index + 1];
|
|
109
|
+
if (state === "string") {
|
|
110
|
+
result += character;
|
|
111
|
+
if (escaped) escaped = false;
|
|
112
|
+
else if (character === "\\") escaped = true;
|
|
113
|
+
else if (character === quote) {
|
|
114
|
+
state = "code";
|
|
115
|
+
quote = null;
|
|
116
|
+
}
|
|
117
|
+
} else if (state === "line-comment") {
|
|
118
|
+
if (character === "\n" || character === "\r") {
|
|
119
|
+
result += character;
|
|
120
|
+
state = "code";
|
|
121
|
+
} else result += " ";
|
|
122
|
+
} else if (state === "block-comment") {
|
|
123
|
+
if (character === "*" && next === "/") {
|
|
124
|
+
result += " ";
|
|
125
|
+
index += 1;
|
|
126
|
+
state = "code";
|
|
127
|
+
} else result += character === "\n" || character === "\r" ? character : " ";
|
|
128
|
+
} else if (character === "\"" || character === "'") {
|
|
129
|
+
result += character;
|
|
130
|
+
quote = character;
|
|
131
|
+
state = "string";
|
|
132
|
+
} else if (character === "/" && next === "/") {
|
|
133
|
+
result += " ";
|
|
134
|
+
index += 1;
|
|
135
|
+
state = "line-comment";
|
|
136
|
+
} else if (character === "/" && next === "*") {
|
|
137
|
+
result += " ";
|
|
138
|
+
index += 1;
|
|
139
|
+
state = "block-comment";
|
|
140
|
+
} else result += character;
|
|
141
|
+
}
|
|
142
|
+
return state === "code" ? result : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const JAVA_PLUGIN_IDS = new Set(["java", "java-library", "java-platform", "java-gradle-plugin", "application", "war"]);
|
|
146
|
+
|
|
147
|
+
function quotedPluginId(rawBlock, start) {
|
|
148
|
+
const plugin = rawBlock.slice(start).match(/^\s*(?:\(\s*)?["']([^"']+)["']/u)?.[1];
|
|
149
|
+
return JAVA_PLUGIN_IDS.has(plugin);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function pluginBlockHasJava(rawBlock) {
|
|
153
|
+
const masked = maskGradleStringsAndComments(rawBlock);
|
|
154
|
+
if (masked === null) return false;
|
|
155
|
+
for (const match of masked.matchAll(/\bid\b/gu)) {
|
|
156
|
+
if (quotedPluginId(rawBlock, match.index + match[0].length)) return true;
|
|
157
|
+
}
|
|
158
|
+
return /(?:^|[;{}\n])\s*(?:`(?:java|javaLibrary|javaPlatform|java-gradle-plugin|application|war)`|java|javaLibrary|javaPlatform|java-gradle-plugin|application|war)\s*(?:[;\n}]|$)/mu.test(masked);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function hasJavaPlugin(text) {
|
|
162
|
+
const masked = maskGradleStringsAndComments(text);
|
|
163
|
+
if (masked === null) return false;
|
|
164
|
+
for (const match of masked.matchAll(/\bplugins\s*\{([\s\S]*?)\}/gu)) {
|
|
165
|
+
const open = match[0].indexOf("{");
|
|
166
|
+
if (pluginBlockHasJava(text.slice(match.index + open + 1, match.index + match[0].length - 1))) return true;
|
|
167
|
+
}
|
|
168
|
+
for (const match of masked.matchAll(/\bapply\s+plugin\s*:\s*/gu)) {
|
|
169
|
+
const suffix = text.slice(match.index + match[0].length);
|
|
170
|
+
if (/^['"](?:java|java-library|java-platform|java-gradle-plugin|application|war)['"]/u.test(suffix)) return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function splitGradleArguments(text) {
|
|
176
|
+
if (text.trim() === "") return [];
|
|
177
|
+
const parts = [];
|
|
178
|
+
let start = 0;
|
|
179
|
+
let quote = null;
|
|
180
|
+
let escaped = false;
|
|
181
|
+
let depth = 0;
|
|
182
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
183
|
+
const character = text[index];
|
|
184
|
+
if (quote) {
|
|
185
|
+
if (escaped) escaped = false;
|
|
186
|
+
else if (character === "\\") escaped = true;
|
|
187
|
+
else if (character === quote) quote = null;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (character === "\"" || character === "'") {
|
|
191
|
+
quote = character;
|
|
192
|
+
} else if (character === "(" || character === "[" || character === "{") {
|
|
193
|
+
depth += 1;
|
|
194
|
+
} else if (character === ")" || character === "]" || character === "}") {
|
|
195
|
+
depth -= 1;
|
|
196
|
+
if (depth < 0) return null;
|
|
197
|
+
} else if (character === "," && depth === 0) {
|
|
198
|
+
parts.push(text.slice(start, index).trim());
|
|
199
|
+
start = index + 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (quote || depth !== 0) return null;
|
|
203
|
+
parts.push(text.slice(start).trim());
|
|
204
|
+
return parts.filter(Boolean);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function gradleQuotedValue(value) {
|
|
208
|
+
const trimmed = value.trim();
|
|
209
|
+
if (trimmed.length < 2 || !["\"", "'"].includes(trimmed[0]) || trimmed.at(-1) !== trimmed[0]) return null;
|
|
210
|
+
const quote = trimmed[0];
|
|
211
|
+
if (trimmed.includes("$")) return null;
|
|
212
|
+
let result = "";
|
|
213
|
+
let escaped = false;
|
|
214
|
+
for (let index = 1; index < trimmed.length; index += 1) {
|
|
215
|
+
const character = trimmed[index];
|
|
216
|
+
if (escaped) {
|
|
217
|
+
result += character;
|
|
218
|
+
escaped = false;
|
|
219
|
+
} else if (character === "\\") {
|
|
220
|
+
escaped = true;
|
|
221
|
+
} else if (character === quote) {
|
|
222
|
+
return index === trimmed.length - 1 ? result : null;
|
|
223
|
+
} else {
|
|
224
|
+
result += character;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function gradleParenthesisEnd(source, open) {
|
|
231
|
+
let depth = 1;
|
|
232
|
+
for (let index = open + 1; index < source.length; index += 1) {
|
|
233
|
+
if (source[index] === "(") depth += 1;
|
|
234
|
+
else if (source[index] === ")") {
|
|
235
|
+
depth -= 1;
|
|
236
|
+
if (depth === 0) return index;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function topLevelGradleIncludeOffsets(source) {
|
|
243
|
+
const offsets = [];
|
|
244
|
+
const depths = { braces: 0, parentheses: 0, brackets: 0 };
|
|
245
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
246
|
+
const character = source[index];
|
|
247
|
+
if (character === "{") depths.braces += 1;
|
|
248
|
+
else if (character === "}") depths.braces = Math.max(0, depths.braces - 1);
|
|
249
|
+
else if (character === "(") depths.parentheses += 1;
|
|
250
|
+
else if (character === ")") depths.parentheses = Math.max(0, depths.parentheses - 1);
|
|
251
|
+
else if (character === "[") depths.brackets += 1;
|
|
252
|
+
else if (character === "]") depths.brackets = Math.max(0, depths.brackets - 1);
|
|
253
|
+
if (Object.values(depths).some((depth) => depth !== 0)
|
|
254
|
+
|| !source.startsWith("include", index)
|
|
255
|
+
|| /[A-Za-z0-9_]/u.test(source[index - 1] ?? "")
|
|
256
|
+
|| /[A-Za-z0-9_]/u.test(source[index + 7] ?? "")) continue;
|
|
257
|
+
offsets.push(index);
|
|
258
|
+
}
|
|
259
|
+
return offsets;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseGradleIncludeAt(text, source, index) {
|
|
263
|
+
let cursor = index + 7;
|
|
264
|
+
while (/\s/u.test(text[cursor] ?? "")) cursor += 1;
|
|
265
|
+
let body;
|
|
266
|
+
if (text[cursor] === "(") {
|
|
267
|
+
const close = gradleParenthesisEnd(source, cursor);
|
|
268
|
+
if (close < 0) return null;
|
|
269
|
+
body = text.slice(cursor + 1, close);
|
|
270
|
+
} else {
|
|
271
|
+
const end = text.indexOf("\n", cursor);
|
|
272
|
+
body = text.slice(cursor, end < 0 ? text.length : end);
|
|
273
|
+
}
|
|
274
|
+
const commentFreeBody = stripGradleComments(body);
|
|
275
|
+
const values = commentFreeBody === null ? null : splitGradleArguments(commentFreeBody);
|
|
276
|
+
if (values === null) return null;
|
|
277
|
+
const literals = values.map((value) => gradleQuotedValue(value));
|
|
278
|
+
return literals.some((literal) => literal === null || literal.trim() === "") ? null : literals;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isStandaloneGradleInclude(source, index) {
|
|
282
|
+
const statementStart = Math.max(
|
|
283
|
+
source.lastIndexOf("\n", index - 1),
|
|
284
|
+
source.lastIndexOf("\r", index - 1),
|
|
285
|
+
source.lastIndexOf(";", index - 1),
|
|
286
|
+
) + 1;
|
|
287
|
+
return source.slice(statementStart, index).trim() === "";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isUnbracedGradleControlBody(source, index) {
|
|
291
|
+
const lineStart = Math.max(source.lastIndexOf("\n", index - 1), source.lastIndexOf("\r", index - 1)) + 1;
|
|
292
|
+
let previousEnd = lineStart - 1;
|
|
293
|
+
while (previousEnd >= 0) {
|
|
294
|
+
const previousStart = Math.max(
|
|
295
|
+
source.lastIndexOf("\n", previousEnd - 1),
|
|
296
|
+
source.lastIndexOf("\r", previousEnd - 1),
|
|
297
|
+
) + 1;
|
|
298
|
+
const previousLine = source.slice(previousStart, previousEnd).trim();
|
|
299
|
+
if (previousLine) {
|
|
300
|
+
if (previousLine.includes("{")) return false;
|
|
301
|
+
return /^(?:if|else|for|while|when|switch)\b/u.test(previousLine);
|
|
302
|
+
}
|
|
303
|
+
previousEnd = previousStart - 1;
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function parseGradleSettings(text) {
|
|
309
|
+
const invalid = {
|
|
310
|
+
valid: false,
|
|
311
|
+
javaPlugin: false,
|
|
312
|
+
javaPlatform: false,
|
|
313
|
+
javaCompiler: false,
|
|
314
|
+
packaging: null,
|
|
315
|
+
modules: [],
|
|
316
|
+
includes: [],
|
|
317
|
+
gradleBuild: false,
|
|
318
|
+
gradleSettings: true,
|
|
319
|
+
gradleProperties: false,
|
|
320
|
+
};
|
|
321
|
+
if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalid;
|
|
322
|
+
const source = maskGradleStringsAndComments(text);
|
|
323
|
+
if (source === null) return invalid;
|
|
324
|
+
const includes = [];
|
|
325
|
+
for (const offset of topLevelGradleIncludeOffsets(source)) {
|
|
326
|
+
if (!isStandaloneGradleInclude(source, offset) || isUnbracedGradleControlBody(source, offset)) continue;
|
|
327
|
+
const literals = parseGradleIncludeAt(text, source, offset);
|
|
328
|
+
if (literals === null) return invalid;
|
|
329
|
+
includes.push(...literals);
|
|
330
|
+
}
|
|
331
|
+
return { ...invalid, valid: true, includes: [...new Set(includes)] };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function parseGradleProperties(text) {
|
|
335
|
+
if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidJava();
|
|
336
|
+
return { ...invalidJava(), valid: true, gradleProperties: true };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function parseGradleBuild(text) {
|
|
340
|
+
if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidJava();
|
|
341
|
+
const source = maskGradleStringsAndComments(text);
|
|
342
|
+
if (source === null) return invalidJava();
|
|
343
|
+
const javaPlugin = hasJavaPlugin(text);
|
|
344
|
+
const javaPlatform = javaPlugin && /(?:java-platform|javaPlatform)/u.test(text);
|
|
345
|
+
return {
|
|
346
|
+
valid: javaPlugin || javaPlatform,
|
|
347
|
+
javaPlugin,
|
|
348
|
+
javaPlatform,
|
|
349
|
+
javaCompiler: false,
|
|
350
|
+
packaging: null,
|
|
351
|
+
modules: [],
|
|
352
|
+
includes: [],
|
|
353
|
+
gradleBuild: true,
|
|
354
|
+
gradleSettings: false,
|
|
355
|
+
gradleProperties: false,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function parseBazelJava(text) {
|
|
360
|
+
if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidJava();
|
|
361
|
+
const source = maskBuildScript(text);
|
|
362
|
+
const javaPlugin = source !== null && /\bjava_(?:binary|library|test|import|plugin)\s*\(/u.test(source);
|
|
363
|
+
return { valid: javaPlugin, javaPlugin, javaPlatform: false, javaCompiler: false, packaging: null, modules: [] };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function hasOwnedJavaSource(sourceFiles, targetRoot, projectRoot, projectRoots) {
|
|
367
|
+
return sourceFiles.some((file) => JAVA_SOURCE_EXTENSIONS.has(file.extension)
|
|
368
|
+
&& pathBelongsToRoot(portablePath(targetRoot, file.path), projectRoot, projectRoots));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function inspectJavaProject({ projectRoot, manifestRelative, manifestText, manifestName, targetRoot, sourceFiles, projectRoots }) {
|
|
372
|
+
const parsed = manifestName === "pom.xml"
|
|
373
|
+
? parseMavenPom(manifestText)
|
|
374
|
+
: manifestName === "build" || manifestName === "build.bazel"
|
|
375
|
+
? parseBazelJava(manifestText)
|
|
376
|
+
: manifestName === "settings.gradle" || manifestName === "settings.gradle.kts"
|
|
377
|
+
? parseGradleSettings(manifestText)
|
|
378
|
+
: manifestName === "gradle.properties"
|
|
379
|
+
? parseGradleProperties(manifestText)
|
|
380
|
+
: parseGradleBuild(manifestText);
|
|
381
|
+
const javaSource = hasOwnedJavaSource(sourceFiles, targetRoot, projectRoot, projectRoots);
|
|
382
|
+
const canProvideJavaEvidence = !parsed.gradleSettings && !parsed.gradleProperties;
|
|
383
|
+
const structurallyValid = parsed.valid || parsed.gradleBuild;
|
|
384
|
+
const primary = canProvideJavaEvidence && structurallyValid
|
|
385
|
+
&& (javaSource || parsed.javaPlugin || parsed.javaPlatform || parsed.javaCompiler);
|
|
386
|
+
const primarySignals = primary
|
|
387
|
+
? [`${manifestRelative}:${parsed.javaPlatform ? "java-platform" : manifestName === "pom.xml" ? "maven" : "build"}`]
|
|
388
|
+
: [];
|
|
389
|
+
const supportingSignals = [];
|
|
390
|
+
if (javaSource) supportingSignals.push(`${projectRoot === "." ? "source" : `${projectRoot}/source`}:java`);
|
|
391
|
+
for (const module of parsed.modules) supportingSignals.push(`${manifestRelative}:module=${module}`);
|
|
392
|
+
for (const include of parsed.includes ?? []) supportingSignals.push(`${manifestRelative}:include=${include}`);
|
|
393
|
+
return createLanguageProject({
|
|
394
|
+
kind: "java",
|
|
395
|
+
root: projectRoot,
|
|
396
|
+
manifest: manifestRelative,
|
|
397
|
+
frameworks: primary ? ["java"] : [],
|
|
398
|
+
primary,
|
|
399
|
+
primarySignals,
|
|
400
|
+
supportingSignals,
|
|
401
|
+
internal: { ...parsed, valid: parsed.valid },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const LANGUAGE_PROJECT_FILE_KINDS = Object.freeze({
|
|
4
|
+
"go.mod": ["go"],
|
|
5
|
+
"go.work": ["go-work"],
|
|
6
|
+
"tsconfig.json": ["typescript"],
|
|
7
|
+
"jsconfig.json": ["javascript-config"],
|
|
8
|
+
"composer.json": ["php"],
|
|
9
|
+
"pom.xml": ["java-maven"],
|
|
10
|
+
"build.gradle": ["java-gradle"],
|
|
11
|
+
"build.gradle.kts": ["java-gradle"],
|
|
12
|
+
"settings.gradle": ["java-gradle-settings"],
|
|
13
|
+
"settings.gradle.kts": ["java-gradle-settings"],
|
|
14
|
+
"gradle.properties": ["java-gradle-properties"],
|
|
15
|
+
"build": ["java-bazel", "native-bazel"],
|
|
16
|
+
"build.bazel": ["java-bazel", "native-bazel"],
|
|
17
|
+
"cmakelists.txt": ["c-cpp-cmake", "swift-cmake"],
|
|
18
|
+
"meson.build": ["c-cpp-meson", "swift-meson"],
|
|
19
|
+
"meson_options.txt": ["c-cpp-meson-options"],
|
|
20
|
+
"makefile": ["c-cpp-make"],
|
|
21
|
+
"gnumakefile": ["c-cpp-make"],
|
|
22
|
+
"package.swift": ["swift-package"],
|
|
23
|
+
"project.pbxproj": ["swift-xcode"],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const LANGUAGE_SOURCE_EXTENSIONS = Object.freeze(new Set([
|
|
27
|
+
".c", ".cc", ".cpp", ".cxx", ".c++",
|
|
28
|
+
".go", ".java", ".php", ".sql", ".psql", ".pgsql", ".swift",
|
|
29
|
+
".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
|
|
30
|
+
]));
|
|
31
|
+
|
|
32
|
+
export const SQL_SOURCE_EXTENSIONS = Object.freeze(new Set([".sql", ".psql", ".pgsql"]));
|
|
33
|
+
export const C_SOURCE_EXTENSIONS = Object.freeze(new Set([".c"]));
|
|
34
|
+
export const CPP_SOURCE_EXTENSIONS = Object.freeze(new Set([".cc", ".cpp", ".cxx", ".c++", ".C"]));
|
|
35
|
+
export const JAVA_SOURCE_EXTENSIONS = Object.freeze(new Set([".java"]));
|
|
36
|
+
export const PHP_SOURCE_EXTENSIONS = Object.freeze(new Set([".php", ".phtml", ".php3", ".php4", ".php5", ".php7", ".phps"]));
|
|
37
|
+
export const SWIFT_SOURCE_EXTENSIONS = Object.freeze(new Set([".swift"]));
|
|
38
|
+
|
|
39
|
+
export function languageProjectKinds(fileName) {
|
|
40
|
+
if (typeof fileName !== "string") return [];
|
|
41
|
+
const lowerName = fileName.toLowerCase();
|
|
42
|
+
if (/^tsconfig\.[^/]+\.json$/u.test(lowerName)) return ["typescript"];
|
|
43
|
+
if (/\.sqlproj$/u.test(lowerName)) return ["sql-project"];
|
|
44
|
+
return LANGUAGE_PROJECT_FILE_KINDS[lowerName] ?? [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function languageSourceExtension(fileName) {
|
|
48
|
+
if (typeof fileName !== "string") return null;
|
|
49
|
+
const extension = path.extname(fileName);
|
|
50
|
+
const normalized = extension === ".C" ? ".C" : extension.toLowerCase();
|
|
51
|
+
return LANGUAGE_SOURCE_EXTENSIONS.has(normalized.toLowerCase()) ? normalized : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function portablePath(root, absolutePath) {
|
|
55
|
+
const relative = path.relative(root, absolutePath).split(path.sep).join("/");
|
|
56
|
+
return relative === "" ? "." : relative;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function relativeProjectRoots(targetRoot, projectFiles = []) {
|
|
60
|
+
return [...new Set(projectFiles
|
|
61
|
+
.map((entry) => portablePath(targetRoot, path.dirname(entry.path)))
|
|
62
|
+
.filter((root) => root !== ".." && !root.startsWith("../")))].sort((left, right) => left.localeCompare(right));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function pathBelongsToRoot(relativePath, root, projectRoots = []) {
|
|
66
|
+
const normalizedPath = relativePath.replaceAll("\\", "/").toLowerCase();
|
|
67
|
+
const normalizedRoot = root.replaceAll("\\", "/").toLowerCase();
|
|
68
|
+
if (normalizedRoot !== "."
|
|
69
|
+
&& normalizedPath !== normalizedRoot
|
|
70
|
+
&& !normalizedPath.startsWith(`${normalizedRoot}/`)) return false;
|
|
71
|
+
return !projectRoots.some((candidate) => {
|
|
72
|
+
const normalizedCandidate = candidate.replaceAll("\\", "/").toLowerCase();
|
|
73
|
+
const nested = normalizedRoot === "."
|
|
74
|
+
? normalizedCandidate !== "."
|
|
75
|
+
: normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
76
|
+
return nested
|
|
77
|
+
&& (normalizedPath === normalizedCandidate || normalizedPath.startsWith(`${normalizedCandidate}/`));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function sourceFilesUnderRoot(sourceFiles, targetRoot, root, extensions, projectRoots = []) {
|
|
82
|
+
return sourceFiles.filter((file) => {
|
|
83
|
+
if (!extensions.has(languageSourceExtension(file.name))) return false;
|
|
84
|
+
return pathBelongsToRoot(portablePath(targetRoot, file.path), root, projectRoots);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createLanguageProject({
|
|
89
|
+
kind,
|
|
90
|
+
root,
|
|
91
|
+
manifest,
|
|
92
|
+
frameworks = [],
|
|
93
|
+
primary = false,
|
|
94
|
+
primarySignals = [],
|
|
95
|
+
supportingSignals = [],
|
|
96
|
+
internal = null,
|
|
97
|
+
}) {
|
|
98
|
+
return {
|
|
99
|
+
kind,
|
|
100
|
+
root,
|
|
101
|
+
manifest,
|
|
102
|
+
validManifest: Boolean(internal?.valid ?? primary),
|
|
103
|
+
primary,
|
|
104
|
+
frameworks: [...new Set(frameworks)].sort((left, right) => left.localeCompare(right)),
|
|
105
|
+
primarySignals: [...primarySignals],
|
|
106
|
+
supportingSignals: [...supportingSignals],
|
|
107
|
+
internal,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isDirectSourceClaim(claim, extensions) {
|
|
112
|
+
return extensions.has(path.extname(claim).toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isConfigOnlyProject(project) {
|
|
116
|
+
return !project?.frameworks?.length && !project?.primary;
|
|
117
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const MAX_EXPLANATION_TEXT = 240;
|
|
2
|
+
const MAX_EXPLANATION_REASON_CODE = 96;
|
|
3
|
+
const MAX_EXPLANATION_ARTIFACT_TEXT = 240;
|
|
4
|
+
const MAX_EXPLANATION_REASONS = 3;
|
|
5
|
+
const MAX_EXPLANATION_ARTIFACTS = 6;
|
|
6
|
+
|
|
7
|
+
const REQUIRED_CHANGES = Object.freeze({
|
|
8
|
+
E_CONTRACT_STALE: "Refresh the contract-bound checkpoint and route before continuing.",
|
|
9
|
+
E_ROUTE_STALE: "Re-run the canonical route for the current contract before continuing.",
|
|
10
|
+
E_REPOSITORY_CHANGED: "Revalidate the repository and record new evidence before continuing.",
|
|
11
|
+
E_RECEIPT_STALE: "Inspect the receipt boundary and obtain a fresh independently verifiable receipt.",
|
|
12
|
+
E_RECEIPT_MISSING: "Provide the required task-bound receipt or record the check as unavailable.",
|
|
13
|
+
E_CHECK_OBSERVATION_REQUIRED: "Run the required check through an observable verification boundary and record its result.",
|
|
14
|
+
E_TASK_SCOPE_CONFLICT: "Resolve the overlapping task ownership through the canonical task recovery or coordination path.",
|
|
15
|
+
E_VERIFICATION_EXECUTION_INVALID: "Repair or replace the verification execution boundary before recording evidence.",
|
|
16
|
+
E_PUBLICATION_REQUIREMENT_PENDING: "Obtain independently verifiable publication evidence; do not infer publication from local checks.",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function bounded(value, limit = MAX_EXPLANATION_TEXT) {
|
|
20
|
+
const text = String(value ?? "").trim();
|
|
21
|
+
return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function reasonExplanation(reason) {
|
|
25
|
+
const source = reason && typeof reason === "object" ? reason : {};
|
|
26
|
+
const artifacts = [...new Set((Array.isArray(source.artifacts) ? source.artifacts : [])
|
|
27
|
+
.filter((artifact) => typeof artifact === "string" && artifact.length > 0)
|
|
28
|
+
.map((artifact) => bounded(artifact, MAX_EXPLANATION_ARTIFACT_TEXT)))]
|
|
29
|
+
.slice(0, MAX_EXPLANATION_ARTIFACTS);
|
|
30
|
+
return {
|
|
31
|
+
code: bounded(source.code ?? "E_NEXT_ACTION_BLOCKED", MAX_EXPLANATION_REASON_CODE),
|
|
32
|
+
failedInvariant: bounded(source.message),
|
|
33
|
+
safeArtifacts: artifacts,
|
|
34
|
+
requiredChange: REQUIRED_CHANGES[source.code] ?? "Satisfy the canonical condition described by this reason before continuing.",
|
|
35
|
+
evidenceNeeded: artifacts.length > 0
|
|
36
|
+
? `Inspect or update only the listed protocol artifacts, then re-run the canonical next action.`
|
|
37
|
+
: "Provide a fresh, scoped, independently observable result for the blocked condition.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function explainNextAction(result) {
|
|
42
|
+
const reasons = (Array.isArray(result?.reasons) ? result.reasons : [])
|
|
43
|
+
.slice(0, MAX_EXPLANATION_REASONS)
|
|
44
|
+
.map(reasonExplanation);
|
|
45
|
+
const mutating = (Array.isArray(result?.commandSpecs) ? result.commandSpecs : []).some((spec) => [
|
|
46
|
+
"advance",
|
|
47
|
+
"clear-state",
|
|
48
|
+
"complete",
|
|
49
|
+
"record-check",
|
|
50
|
+
"record-diagnosis",
|
|
51
|
+
"record-intervention",
|
|
52
|
+
"prepare-completion",
|
|
53
|
+
"route",
|
|
54
|
+
].includes(spec.commandId));
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
summary: bounded(`Next action ${result?.nextAction ?? "UNKNOWN"} is required for phase ${result?.currentPhase ?? "UNKNOWN"}.`),
|
|
58
|
+
actionKind: mutating ? "MUTATING_COMMAND_MAY_BE_REQUIRED" : "READ_ONLY_OR_HOST_ACTION",
|
|
59
|
+
requiresExternalAuthority: Boolean(result?.authorityRequired || result?.hostActionRequired || result?.approvalRequired),
|
|
60
|
+
reasons,
|
|
61
|
+
bounded: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createLanguageProject, PHP_SOURCE_EXTENSIONS } from "./multi-language-project.js";
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function invalidComposer() {
|
|
8
|
+
return {
|
|
9
|
+
valid: false,
|
|
10
|
+
name: null,
|
|
11
|
+
type: null,
|
|
12
|
+
autoload: false,
|
|
13
|
+
requirements: [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasValidComposerSections(value) {
|
|
18
|
+
const requirementFields = ["require", "require-dev", "replace", "provide", "conflict"];
|
|
19
|
+
if (requirementFields.some((field) => value[field] !== undefined && !isPlainObject(value[field]))) return false;
|
|
20
|
+
const autoload = value.autoload === undefined || isPlainObject(value.autoload);
|
|
21
|
+
const autoloadDev = value["autoload-dev"] === undefined || isPlainObject(value["autoload-dev"]);
|
|
22
|
+
return autoload && autoloadDev;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function composerStringField(value, field) {
|
|
26
|
+
if (value[field] === undefined) return null;
|
|
27
|
+
return typeof value[field] === "string" && value[field].trim() !== "" ? value[field] : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasComposerIdentity(value) {
|
|
31
|
+
return value.name !== undefined
|
|
32
|
+
|| value.require !== undefined
|
|
33
|
+
|| value.autoload !== undefined
|
|
34
|
+
|| value["autoload-dev"] !== undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseComposerValue(value) {
|
|
38
|
+
if (!isPlainObject(value) || !hasValidComposerSections(value)) return invalidComposer();
|
|
39
|
+
const name = composerStringField(value, "name");
|
|
40
|
+
const type = composerStringField(value, "type");
|
|
41
|
+
if (name === undefined || type === undefined || !hasComposerIdentity(value)) return invalidComposer();
|
|
42
|
+
const hasPhpIdentity = name !== null || value.require !== undefined || value.autoload !== undefined || value["autoload-dev"] !== undefined;
|
|
43
|
+
if (!hasPhpIdentity) return invalidComposer();
|
|
44
|
+
return {
|
|
45
|
+
valid: true,
|
|
46
|
+
name,
|
|
47
|
+
type,
|
|
48
|
+
autoload: value.autoload !== undefined || value["autoload-dev"] !== undefined,
|
|
49
|
+
requirements: Object.keys(value.require ?? {}).sort((left, right) => left.localeCompare(right)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseComposerJson(text) {
|
|
54
|
+
if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidComposer();
|
|
55
|
+
try {
|
|
56
|
+
return parseComposerValue(JSON.parse(text));
|
|
57
|
+
} catch {
|
|
58
|
+
return invalidComposer();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function phpSourceLooksExecutable(text) {
|
|
63
|
+
return typeof text === "string" && /<\?(?:php(?:\s|$)|=)/iu.test(text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function inspectPhpProject({ projectRoot, manifestRelative, manifestText }) {
|
|
67
|
+
const parsed = parseComposerJson(manifestText);
|
|
68
|
+
const primary = parsed.valid;
|
|
69
|
+
const primarySignals = primary ? [`${manifestRelative}:composer`] : [];
|
|
70
|
+
const supportingSignals = [];
|
|
71
|
+
if (parsed.name) supportingSignals.push(`${manifestRelative}:name=${parsed.name}`);
|
|
72
|
+
if (parsed.type) supportingSignals.push(`${manifestRelative}:type=${parsed.type}`);
|
|
73
|
+
if (parsed.autoload) supportingSignals.push(`${manifestRelative}:autoload`);
|
|
74
|
+
for (const requirement of parsed.requirements) supportingSignals.push(`${manifestRelative}:require=${requirement}`);
|
|
75
|
+
return createLanguageProject({
|
|
76
|
+
kind: "php",
|
|
77
|
+
root: projectRoot,
|
|
78
|
+
manifest: manifestRelative,
|
|
79
|
+
frameworks: primary ? ["php"] : [],
|
|
80
|
+
primary,
|
|
81
|
+
primarySignals,
|
|
82
|
+
supportingSignals,
|
|
83
|
+
internal: { ...parsed, phpSourceExtensions: [...PHP_SOURCE_EXTENSIONS] },
|
|
84
|
+
});
|
|
85
|
+
}
|