forgeos 0.1.0-alpha.61 → 0.1.0-alpha.63
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/AGENTS.md +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/adapters/java/pom.xml +13 -0
- package/adapters/java-spring-boot-starter/pom.xml +13 -0
- package/docs/changelog.md +14 -0
- package/examples/java-billing/pom.xml +8 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/brownfield-import/index.ts +106 -11
- package/src/forge/brownfield-import/types.ts +1 -1
- package/src/forge/cli/commands.ts +24 -14
- package/src/forge/version.ts +1 -1
- package/src/forge/workspace/change-summary.ts +13 -1
- package/templates/b2b-support-web/web/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.63 input=6671cc4ce9f1aeab62768f037489379d8303a79d2681b1fae4cb42a84e818b30 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.63
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Make generated freshness and Java/Spring brownfield analysis trustworthy.
|
|
8
|
+
|
|
9
|
+
- `forge status` now executes deterministic generation verification, so a
|
|
10
|
+
clean Git worktree can no longer hide committed generated drift that
|
|
11
|
+
`forge handoff` would reject.
|
|
12
|
+
- Report compact generation evidence in status output, including freshness,
|
|
13
|
+
changed artifact counts, samples, and diagnostic codes.
|
|
14
|
+
- Extend `forge import analyze` to inventory Maven dependencies, recognize
|
|
15
|
+
Spring Boot, scan Java source, discover Spring MVC controller mappings, and
|
|
16
|
+
detect `System.getenv(...)` usage.
|
|
17
|
+
- Keep Spring-derived command/query candidates static, review-required, and
|
|
18
|
+
hidden from agents under the existing brownfield safety model.
|
|
19
|
+
- Pin a modern Maven compiler plugin for the Java adapter and Spring starter,
|
|
20
|
+
so their declared Java 17 release compiles on the supported JDK 21 toolchain.
|
|
21
|
+
- Make example fixture seeding replace package directories cleanly, avoiding
|
|
22
|
+
cross-identity timestamp/ownership failures in shared development workspaces.
|
|
23
|
+
|
|
24
|
+
## 0.1.0-alpha.62
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- Keep commit-ready change views from exposing local secrets or WorkOS state.
|
|
29
|
+
|
|
30
|
+
- Classify real `.env*` files as operational while keeping `.env.example`
|
|
31
|
+
and `.env.*.example` files commit-ready.
|
|
32
|
+
- Exclude `.workos-seed-state.json` and `.workos-fga-state.json` from
|
|
33
|
+
commit-ready summaries.
|
|
34
|
+
- Add regression coverage so deploy examples stay visible while
|
|
35
|
+
`deploy/.env.production` remains excluded.
|
|
36
|
+
|
|
3
37
|
## 0.1.0-alpha.61
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -309,7 +309,7 @@ Common command groups:
|
|
|
309
309
|
| `forge release` | Release/source-map bridge and symbolication |
|
|
310
310
|
| `forge agent`, `forge agent-contract` | Agent-facing contract, adapter exports, context, install, ingest, and memory commands |
|
|
311
311
|
| `forge mcp serve` | MCP server for external agents to read Forge context, memory, timeline, and inspect surfaces |
|
|
312
|
-
| `forge import analyze`, `forge import inspect` | Brownfield TypeScript/JavaScript app inventory
|
|
312
|
+
| `forge import analyze`, `forge import inspect` | Brownfield TypeScript/JavaScript and Java/Spring app inventory with reviewed migration planning |
|
|
313
313
|
| `forge inspect imported --json` | Inspect `.forge/import` analysis artifacts |
|
|
314
314
|
| `forge self-host` | Self-host packaging and checks |
|
|
315
315
|
|
package/adapters/java/pom.xml
CHANGED
|
@@ -31,4 +31,17 @@
|
|
|
31
31
|
<version>${jackson.version}</version>
|
|
32
32
|
</dependency>
|
|
33
33
|
</dependencies>
|
|
34
|
+
|
|
35
|
+
<build>
|
|
36
|
+
<plugins>
|
|
37
|
+
<plugin>
|
|
38
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
39
|
+
<artifactId>maven-compiler-plugin</artifactId>
|
|
40
|
+
<version>3.13.0</version>
|
|
41
|
+
<configuration>
|
|
42
|
+
<release>${maven.compiler.release}</release>
|
|
43
|
+
</configuration>
|
|
44
|
+
</plugin>
|
|
45
|
+
</plugins>
|
|
46
|
+
</build>
|
|
34
47
|
</project>
|
|
@@ -33,4 +33,17 @@
|
|
|
33
33
|
<version>6.1.14</version>
|
|
34
34
|
</dependency>
|
|
35
35
|
</dependencies>
|
|
36
|
+
|
|
37
|
+
<build>
|
|
38
|
+
<plugins>
|
|
39
|
+
<plugin>
|
|
40
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
41
|
+
<artifactId>maven-compiler-plugin</artifactId>
|
|
42
|
+
<version>3.13.0</version>
|
|
43
|
+
<configuration>
|
|
44
|
+
<release>${maven.compiler.release}</release>
|
|
45
|
+
</configuration>
|
|
46
|
+
</plugin>
|
|
47
|
+
</plugins>
|
|
48
|
+
</build>
|
|
36
49
|
</project>
|
package/docs/changelog.md
CHANGED
|
@@ -4,6 +4,20 @@ Release history for the `forgeos` npm package.
|
|
|
4
4
|
|
|
5
5
|
The canonical source file in the repository is `CHANGELOG.md`.
|
|
6
6
|
|
|
7
|
+
## 0.1.0-alpha.63
|
|
8
|
+
|
|
9
|
+
- `forge status` now verifies generated output deterministically instead of
|
|
10
|
+
inferring freshness from Git state, preventing committed stale artifacts from
|
|
11
|
+
appearing ready while `forge handoff` rejects them.
|
|
12
|
+
- Brownfield import now understands Java/Spring Boot projects: Maven dependency
|
|
13
|
+
inventory, Spring MVC mappings, Java handler scoping, path variables, and
|
|
14
|
+
`System.getenv(...)` usage.
|
|
15
|
+
- Imported Spring operations retain the same conservative review and agent
|
|
16
|
+
visibility defaults as TypeScript/JavaScript candidates.
|
|
17
|
+
- Java adapter builds now pin a release-aware Maven compiler plugin, and example
|
|
18
|
+
fixture setup safely replaces generated package directories in shared
|
|
19
|
+
workspaces.
|
|
20
|
+
|
|
7
21
|
## 0.1.0-alpha.61
|
|
8
22
|
|
|
9
23
|
- Added `forge golden-path plan/status` as the canonical
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
|
|
25
25
|
<build>
|
|
26
26
|
<plugins>
|
|
27
|
+
<plugin>
|
|
28
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
29
|
+
<artifactId>maven-compiler-plugin</artifactId>
|
|
30
|
+
<version>3.13.0</version>
|
|
31
|
+
<configuration>
|
|
32
|
+
<release>${maven.compiler.release}</release>
|
|
33
|
+
</configuration>
|
|
34
|
+
</plugin>
|
|
27
35
|
<plugin>
|
|
28
36
|
<groupId>org.apache.maven.plugins</groupId>
|
|
29
37
|
<artifactId>maven-shade-plugin</artifactId>
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.63","releaseId":"forgeos@0.1.0-alpha.63+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.63 input=6671cc4ce9f1aeab62768f037489379d8303a79d2681b1fae4cb42a84e818b30 content=407b2f82bfffde0fd6fec96fc9130364878d1f2a6d0945e40da2b05d49aea75a
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.63",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.63+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
} from "./types.ts";
|
|
17
17
|
|
|
18
18
|
const IMPORT_DIR = ".forge/import";
|
|
19
|
-
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
19
|
+
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java"]);
|
|
20
20
|
const IGNORED_DIRS = new Set([
|
|
21
21
|
".git",
|
|
22
22
|
".forge",
|
|
@@ -130,8 +130,10 @@ function buildDependencyInventory(workspaceRoot: string): {
|
|
|
130
130
|
dependencies: ImportedDependencyInventory;
|
|
131
131
|
} {
|
|
132
132
|
const pkg = readPackageJson(workspaceRoot);
|
|
133
|
-
const
|
|
133
|
+
const npmDependencies = objectKeys(pkg.dependencies);
|
|
134
134
|
const devDependencies = objectKeys(pkg.devDependencies);
|
|
135
|
+
const pom = readMavenInventory(workspaceRoot);
|
|
136
|
+
const dependencies = [...new Set([...npmDependencies, ...pom.dependencies])].sort();
|
|
135
137
|
const all = [...dependencies, ...devDependencies];
|
|
136
138
|
const scripts = objectKeys(pkg.scripts);
|
|
137
139
|
const frameworks = [
|
|
@@ -143,9 +145,11 @@ function buildDependencyInventory(workspaceRoot: string): {
|
|
|
143
145
|
hasAny(all, ["@nestjs/core"]) ? "nest" : null,
|
|
144
146
|
hasAny(all, ["fastify"]) ? "fastify" : null,
|
|
145
147
|
hasAny(all, ["hono"]) ? "hono" : null,
|
|
148
|
+
pom.frameworks.includes("spring-boot") ? "spring-boot" : null,
|
|
146
149
|
].filter((value): value is string => value !== null);
|
|
147
150
|
const dataPackages = all.filter((name) =>
|
|
148
|
-
["@prisma/client", "prisma", "drizzle-orm", "typeorm", "mongoose", "sequelize", "knex"].includes(name)
|
|
151
|
+
["@prisma/client", "prisma", "drizzle-orm", "typeorm", "mongoose", "sequelize", "knex"].includes(name) ||
|
|
152
|
+
/(?:spring-boot-starter-data-|hibernate-core|mybatis)/u.test(name),
|
|
149
153
|
);
|
|
150
154
|
const externalPackages = all.filter((name) =>
|
|
151
155
|
[
|
|
@@ -163,7 +167,7 @@ function buildDependencyInventory(workspaceRoot: string): {
|
|
|
163
167
|
].includes(name),
|
|
164
168
|
);
|
|
165
169
|
return {
|
|
166
|
-
packageName: typeof pkg.name === "string" ? pkg.name :
|
|
170
|
+
packageName: typeof pkg.name === "string" ? pkg.name : pom.packageName,
|
|
167
171
|
dependencies: {
|
|
168
172
|
dependencies,
|
|
169
173
|
devDependencies,
|
|
@@ -175,6 +179,34 @@ function buildDependencyInventory(workspaceRoot: string): {
|
|
|
175
179
|
};
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
function readMavenInventory(workspaceRoot: string): {
|
|
183
|
+
packageName?: string;
|
|
184
|
+
dependencies: string[];
|
|
185
|
+
frameworks: string[];
|
|
186
|
+
} {
|
|
187
|
+
const pomPath = join(workspaceRoot, "pom.xml");
|
|
188
|
+
if (!existsSync(pomPath)) {
|
|
189
|
+
return { dependencies: [], frameworks: [] };
|
|
190
|
+
}
|
|
191
|
+
const text = readFileSync(pomPath, "utf8");
|
|
192
|
+
const dependencies: string[] = [];
|
|
193
|
+
for (const match of text.matchAll(/<dependency\b[^>]*>([\s\S]*?)<\/dependency>/gu)) {
|
|
194
|
+
const block = match[1] ?? "";
|
|
195
|
+
const groupId = block.match(/<groupId>\s*([^<]+?)\s*<\/groupId>/u)?.[1]?.trim();
|
|
196
|
+
const artifactId = block.match(/<artifactId>\s*([^<]+?)\s*<\/artifactId>/u)?.[1]?.trim();
|
|
197
|
+
if (artifactId) {
|
|
198
|
+
dependencies.push(groupId ? `${groupId}:${artifactId}` : artifactId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const projectWithoutParent = text.replace(/<parent\b[^>]*>[\s\S]*?<\/parent>/u, "");
|
|
202
|
+
const packageName = projectWithoutParent.match(/<artifactId>\s*([^<]+?)\s*<\/artifactId>/u)?.[1]?.trim();
|
|
203
|
+
return {
|
|
204
|
+
...(packageName ? { packageName } : {}),
|
|
205
|
+
dependencies: [...new Set(dependencies)].sort(),
|
|
206
|
+
frameworks: /\b(?:org\.springframework\.boot|spring-boot)[\w.:-]*/u.test(text) ? ["spring-boot"] : [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
178
210
|
function stableId(prefix: string, parts: string[]): string {
|
|
179
211
|
let hash = 2166136261;
|
|
180
212
|
for (const char of parts.join("|")) {
|
|
@@ -185,7 +217,7 @@ function stableId(prefix: string, parts: string[]): string {
|
|
|
185
217
|
}
|
|
186
218
|
|
|
187
219
|
function stripExtension(segment: string): string {
|
|
188
|
-
return segment.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/u, "");
|
|
220
|
+
return segment.replace(/\.(ts|tsx|js|jsx|mjs|cjs|java)$/u, "");
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
function normalizeRouteSegment(segment: string): string | null {
|
|
@@ -219,7 +251,56 @@ function routePathFromFile(relativePath: string, marker: string): string {
|
|
|
219
251
|
}
|
|
220
252
|
|
|
221
253
|
function joinRoutePath(base: string, child: string): string {
|
|
222
|
-
return `/${[base, child].map((part) => part.replace(/^\/|\/$/gu, "")).filter(Boolean).join("/")}
|
|
254
|
+
return `/${[base, child].map((part) => part.replace(/^\/|\/$/gu, "")).filter(Boolean).join("/")}`
|
|
255
|
+
.replace(/\/+/gu, "/")
|
|
256
|
+
.replace(/\{([^}/]+)\}/gu, ":$1");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function springMappingPath(argumentsText: string): string {
|
|
260
|
+
return argumentsText.match(/(?:path|value)\s*=\s*["']([^"']*)["']/u)?.[1] ??
|
|
261
|
+
argumentsText.match(/["']([^"']*)["']/u)?.[1] ??
|
|
262
|
+
"";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function springHandlerName(text: string): string | undefined {
|
|
266
|
+
return text.match(/(?:public|protected|private)?\s*(?:static\s+)?(?:[\w.$<>?,\[\]]+\s+)+(\w+)\s*\(/u)?.[1];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function detectSpringRoutes(file: SourceFile, routes: ImportedRoute[]): void {
|
|
270
|
+
if (extname(file.relativePath) !== ".java" || !/@(?:RestController|Controller)\b/u.test(file.text)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const classIndex = file.text.search(/\bclass\s+\w+/u);
|
|
274
|
+
if (classIndex < 0) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const classAnnotations = file.text.slice(0, classIndex);
|
|
278
|
+
const classMapping = [...classAnnotations.matchAll(/@RequestMapping\s*(?:\(([^)]*)\))?/gu)].at(-1);
|
|
279
|
+
const basePath = springMappingPath(classMapping?.[1] ?? "");
|
|
280
|
+
const methodMappings = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*(?:\(([^)]*)\))?/gu;
|
|
281
|
+
methodMappings.lastIndex = classIndex;
|
|
282
|
+
for (const match of file.text.matchAll(methodMappings)) {
|
|
283
|
+
const annotation = match[1] ?? "RequestMapping";
|
|
284
|
+
const argumentsText = match[2] ?? "";
|
|
285
|
+
const handler = springHandlerName(file.text.slice((match.index ?? 0) + match[0].length, (match.index ?? 0) + match[0].length + 500));
|
|
286
|
+
if (!handler) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const methods = annotation === "RequestMapping"
|
|
290
|
+
? [...argumentsText.matchAll(/RequestMethod\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gu)].map((item) => item[1] ?? "ANY")
|
|
291
|
+
: [annotation.replace(/Mapping$/u, "").toUpperCase()];
|
|
292
|
+
for (const method of methods.length > 0 ? methods : ["ANY"]) {
|
|
293
|
+
addRoute(
|
|
294
|
+
routes,
|
|
295
|
+
method,
|
|
296
|
+
joinRoutePath(basePath, springMappingPath(argumentsText)),
|
|
297
|
+
file.relativePath,
|
|
298
|
+
"spring",
|
|
299
|
+
0.86,
|
|
300
|
+
handler,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
223
304
|
}
|
|
224
305
|
|
|
225
306
|
function pathIncludesRouteMarker(relativePath: string, marker: string): boolean {
|
|
@@ -251,6 +332,7 @@ function detectRoutes(files: SourceFile[]): ImportedRoute[] {
|
|
|
251
332
|
const routes: ImportedRoute[] = [];
|
|
252
333
|
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
253
334
|
for (const file of files) {
|
|
335
|
+
const isJava = extname(file.relativePath) === ".java";
|
|
254
336
|
if (pathIncludesRouteMarker(file.relativePath, "/app/api/") && basename(file.relativePath).startsWith("route.")) {
|
|
255
337
|
const path = routePathFromFile(file.relativePath, "/app/");
|
|
256
338
|
for (const method of methods) {
|
|
@@ -265,12 +347,14 @@ function detectRoutes(files: SourceFile[]): ImportedRoute[] {
|
|
|
265
347
|
addRoute(routes, "ANY", path, file.relativePath, "next-pages-api", 0.78, "default");
|
|
266
348
|
}
|
|
267
349
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
350
|
+
if (!isJava) {
|
|
351
|
+
const expressRoute = /\b(?:app|router)\s*\.\s*(get|post|put|patch|delete|all)\s*\(\s*["'`]([^"'`]+)["'`]/giu;
|
|
352
|
+
for (const match of file.text.matchAll(expressRoute)) {
|
|
353
|
+
addRoute(routes, match[1] ?? "all", match[2] ?? "/", file.relativePath, "express", 0.84);
|
|
354
|
+
}
|
|
271
355
|
}
|
|
272
356
|
|
|
273
|
-
const controller = file.text.match(/@Controller\s*\(\s*["'`]([^"'`]*)["'`]\s*\)/u);
|
|
357
|
+
const controller = !isJava ? file.text.match(/@Controller\s*\(\s*["'`]([^"'`]*)["'`]\s*\)/u) : null;
|
|
274
358
|
if (controller) {
|
|
275
359
|
const nestRoute = /@(Get|Post|Put|Patch|Delete|All)\s*\(\s*(?:["'`]([^"'`]*)["'`])?\s*\)/giu;
|
|
276
360
|
for (const match of file.text.matchAll(nestRoute)) {
|
|
@@ -284,6 +368,7 @@ function detectRoutes(files: SourceFile[]): ImportedRoute[] {
|
|
|
284
368
|
);
|
|
285
369
|
}
|
|
286
370
|
}
|
|
371
|
+
detectSpringRoutes(file, routes);
|
|
287
372
|
}
|
|
288
373
|
const seen = new Set<string>();
|
|
289
374
|
return routes
|
|
@@ -346,6 +431,9 @@ function collectEnv(workspaceRoot: string, files: SourceFile[]): ImportedInvento
|
|
|
346
431
|
for (const match of file.text.matchAll(/\bprocess\.env\.([A-Z0-9_]+)/gu)) {
|
|
347
432
|
names.add(match[1] ?? "");
|
|
348
433
|
}
|
|
434
|
+
for (const match of file.text.matchAll(/\bSystem\.getenv\(\s*["']([A-Z0-9_]+)["']\s*\)/gu)) {
|
|
435
|
+
names.add(match[1] ?? "");
|
|
436
|
+
}
|
|
349
437
|
}
|
|
350
438
|
const envFiles = [".env", ".env.local", ".env.example", ".env.sample"]
|
|
351
439
|
.filter((name) => existsSync(join(workspaceRoot, name)))
|
|
@@ -398,6 +486,13 @@ function scopedSourceTextForRoute(route: ImportedRoute, text: string): string |
|
|
|
398
486
|
/@(Get|Post|Put|Patch|Delete|All)\s*\(/gu,
|
|
399
487
|
);
|
|
400
488
|
}
|
|
489
|
+
if (route.source === "spring" && route.handler) {
|
|
490
|
+
return sliceUntilNextMatch(
|
|
491
|
+
text,
|
|
492
|
+
new RegExp(`@(?:GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)[\\s\\S]{0,500}?\\b${escapeRegExp(route.handler)}\\s*\\(`, "u"),
|
|
493
|
+
/@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\b/gu,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
401
496
|
return null;
|
|
402
497
|
}
|
|
403
498
|
|
|
@@ -451,7 +546,7 @@ function classifyCandidate(route: ImportedRoute, text: string): Pick<ImportedCan
|
|
|
451
546
|
if (tenant) {
|
|
452
547
|
risks.add("tenant-sensitive");
|
|
453
548
|
}
|
|
454
|
-
if (/\bprocess\.env
|
|
549
|
+
if (/\bprocess\.env\.|\bSystem\.getenv\s*\(/u.test(text)) {
|
|
455
550
|
risks.add("secret-sensitive");
|
|
456
551
|
}
|
|
457
552
|
if (methodUnknown) {
|
|
@@ -12,7 +12,7 @@ export interface BrownfieldImportCommandOptions {
|
|
|
12
12
|
export type ImportedAssurance = "static-scan";
|
|
13
13
|
export type ImportedReviewStatus = "needs-review" | "approved" | "rejected";
|
|
14
14
|
export type ImportedEntryKind = "command" | "command-candidate" | "query" | "unknown";
|
|
15
|
-
export type ImportedRouteSource = "next-app-router" | "next-pages-api" | "express" | "nest" | "unknown";
|
|
15
|
+
export type ImportedRouteSource = "next-app-router" | "next-pages-api" | "express" | "nest" | "spring" | "unknown";
|
|
16
16
|
|
|
17
17
|
export interface ImportedDependencyInventory {
|
|
18
18
|
dependencies: string[];
|
|
@@ -732,11 +732,18 @@ export interface StatusCommandResult {
|
|
|
732
732
|
exitCode: 0 | 1;
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
-
export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
|
|
735
|
+
export async function runStatusCommand(workspaceRoot: string): Promise<StatusCommandResult> {
|
|
736
736
|
const summary = buildInspectSummary(workspaceRoot);
|
|
737
737
|
const drift = buildDriftInspect(workspaceRoot);
|
|
738
738
|
const handoff = buildHandoffInspect(workspaceRoot);
|
|
739
739
|
const gitSummary = buildWorkspaceGitSummary(workspaceRoot);
|
|
740
|
+
const generatedVerification = await runGenerateCommand({
|
|
741
|
+
workspaceRoot,
|
|
742
|
+
check: true,
|
|
743
|
+
dryRun: false,
|
|
744
|
+
json: true,
|
|
745
|
+
concurrency: 4,
|
|
746
|
+
});
|
|
740
747
|
const git = {
|
|
741
748
|
available: gitSummary.available,
|
|
742
749
|
...(gitSummary.error ? { error: gitSummary.error } : {}),
|
|
@@ -751,7 +758,8 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
|
|
|
751
758
|
const missingArtifacts = Number(summaryBlock.missingArtifacts ?? 0);
|
|
752
759
|
const tableDrift = Number(driftSummary.tableDrift ?? 0);
|
|
753
760
|
const generatedReady = missingArtifacts === 0;
|
|
754
|
-
const
|
|
761
|
+
const generatedFresh = generatedVerification.exitCode === 0;
|
|
762
|
+
const driftClean = driftSummary.ok === true && generatedFresh;
|
|
755
763
|
const ok = driftClean && generatedReady;
|
|
756
764
|
const handoffDefaultReady = handoffSummary.defaultReady === true;
|
|
757
765
|
const changed = gitSummary.changeSummary.changed;
|
|
@@ -761,24 +769,19 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
|
|
|
761
769
|
changed.byType.source.count +
|
|
762
770
|
changed.byType.config.count +
|
|
763
771
|
changed.byType.operational.count;
|
|
764
|
-
const generatedNeedsCheck = generatedReady && driftClean && generatedGitFiles === 0 && authoredGeneratedInputs > 0;
|
|
765
772
|
const generatedState = !generatedReady
|
|
766
773
|
? "missing-artifacts"
|
|
767
|
-
: generatedNeedsCheck
|
|
768
|
-
? "check-needed"
|
|
769
774
|
: driftClean
|
|
770
775
|
? "ready"
|
|
771
776
|
: "drift";
|
|
772
777
|
const generatedNextActionsRaw = generatedState === "ready"
|
|
773
778
|
? ["forge dev", "forge dev --once --json", "forge generate --check --json"]
|
|
774
|
-
: generatedState === "check-needed"
|
|
775
|
-
? ["forge generate --check --json", "forge handoff --json", "forge dev --once --json"]
|
|
776
779
|
: ["forge generate", "forge check --json", "forge inspect drift --json"];
|
|
777
780
|
const generatedNextActions = forgeCliCommandsForWorkspace(workspaceRoot, generatedNextActionsRaw);
|
|
778
|
-
const generatedGitExplanation =
|
|
779
|
-
?
|
|
780
|
-
|
|
781
|
-
|
|
781
|
+
const generatedGitExplanation = !generatedFresh
|
|
782
|
+
? "deterministic generation verification found stale artifacts; git cleanliness does not prove generated freshness"
|
|
783
|
+
: generatedGitFiles === 0
|
|
784
|
+
? "git status has no generated artifact changes and deterministic generation verification passed"
|
|
782
785
|
: authoredGitFiles === 0
|
|
783
786
|
? "forge generate --check can be clean while git shows generated artifacts changed: generated files match current workspace inputs but differ from HEAD"
|
|
784
787
|
: "git status includes generated artifacts alongside authored changes; review authored inputs first";
|
|
@@ -832,10 +835,18 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
|
|
|
832
835
|
state: generatedState,
|
|
833
836
|
ready: generatedReady,
|
|
834
837
|
driftClean,
|
|
835
|
-
freshness:
|
|
838
|
+
freshness: generatedState === "ready" ? "verified" : generatedFresh ? "attention" : "stale",
|
|
836
839
|
authoredGeneratedInputs,
|
|
837
840
|
missingArtifacts,
|
|
838
841
|
tableDrift,
|
|
842
|
+
verification: {
|
|
843
|
+
checked: true,
|
|
844
|
+
fresh: generatedFresh,
|
|
845
|
+
changedFiles: generatedVerification.changed.length,
|
|
846
|
+
sampleChanged: generatedVerification.changed.slice(0, 8),
|
|
847
|
+
hiddenChanged: Math.max(0, generatedVerification.changed.length - 8),
|
|
848
|
+
diagnosticCodes: [...new Set(generatedVerification.errors.map((diagnostic) => diagnostic.code))],
|
|
849
|
+
},
|
|
839
850
|
safeDevCommand,
|
|
840
851
|
checkCommand: generatedCheckCommand,
|
|
841
852
|
repairCommand: generatedRepairCommand,
|
|
@@ -883,7 +894,6 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
|
|
|
883
894
|
workspaceRoot,
|
|
884
895
|
ok
|
|
885
896
|
? [
|
|
886
|
-
...(generatedState === "check-needed" ? ["forge generate --check --json"] : []),
|
|
887
897
|
"forge handoff --json",
|
|
888
898
|
"forge changed --json",
|
|
889
899
|
"forge dev",
|
|
@@ -2469,7 +2479,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
2469
2479
|
return result.exitCode;
|
|
2470
2480
|
}
|
|
2471
2481
|
case "status": {
|
|
2472
|
-
const result = runStatusCommand(command.workspaceRoot);
|
|
2482
|
+
const result = await runStatusCommand(command.workspaceRoot);
|
|
2473
2483
|
if (command.json) {
|
|
2474
2484
|
process.stdout.write(formatJsonResult(result.data));
|
|
2475
2485
|
} else {
|
package/src/forge/version.ts
CHANGED
|
@@ -78,6 +78,11 @@ export function classifyChangeType(file: string): ChangeType {
|
|
|
78
78
|
const normalized = file.replace(/\\/g, "/");
|
|
79
79
|
const lower = normalized.toLowerCase();
|
|
80
80
|
const basename = lower.split("/").pop() ?? lower;
|
|
81
|
+
const isEnvExample =
|
|
82
|
+
basename === ".env.example" ||
|
|
83
|
+
(basename.startsWith(".env.") && basename.endsWith(".example"));
|
|
84
|
+
const isLocalEnvFile =
|
|
85
|
+
(basename === ".env" || basename.startsWith(".env.")) && !isEnvExample;
|
|
81
86
|
|
|
82
87
|
if (
|
|
83
88
|
lower.startsWith("src/forge/_generated/") ||
|
|
@@ -90,6 +95,13 @@ export function classifyChangeType(file: string): ChangeType {
|
|
|
90
95
|
if (lower === ".codex/hooks.json") {
|
|
91
96
|
return "config";
|
|
92
97
|
}
|
|
98
|
+
if (
|
|
99
|
+
isLocalEnvFile ||
|
|
100
|
+
lower === ".workos-seed-state.json" ||
|
|
101
|
+
lower === ".workos-fga-state.json"
|
|
102
|
+
) {
|
|
103
|
+
return "operational";
|
|
104
|
+
}
|
|
93
105
|
if (lower.startsWith(".playwright-cli/")) {
|
|
94
106
|
return "operational";
|
|
95
107
|
}
|
|
@@ -134,7 +146,7 @@ export function classifyChangeType(file: string): ChangeType {
|
|
|
134
146
|
}
|
|
135
147
|
if (
|
|
136
148
|
basename === "package.json" ||
|
|
137
|
-
|
|
149
|
+
isEnvExample ||
|
|
138
150
|
basename === "bun.lock" ||
|
|
139
151
|
basename === "package-lock.json" ||
|
|
140
152
|
basename === "pnpm-lock.yaml" ||
|