@warpgogol/forge 2.9.0 → 2.9.2

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 (41) hide show
  1. package/dist/os/compass/compass.module.d.ts.map +1 -1
  2. package/dist/os/compass/compass.module.js +32 -16
  3. package/dist/os/compass/compass.module.js.map +1 -1
  4. package/dist/os/compass/handlers/compass-change-summary-handler.d.ts.map +1 -1
  5. package/dist/os/compass/handlers/compass-change-summary-handler.js +13 -2
  6. package/dist/os/compass/handlers/compass-change-summary-handler.js.map +1 -1
  7. package/dist/os/compass/handlers/compass-inventory-handler.d.ts.map +1 -1
  8. package/dist/os/compass/handlers/compass-inventory-handler.js +64 -0
  9. package/dist/os/compass/handlers/compass-inventory-handler.js.map +1 -1
  10. package/dist/os/compass/handlers/compass-inventory.d.ts.map +1 -1
  11. package/dist/os/compass/handlers/compass-inventory.js +51 -7
  12. package/dist/os/compass/handlers/compass-inventory.js.map +1 -1
  13. package/dist/src/onboarding/create.d.ts.map +1 -1
  14. package/dist/src/onboarding/create.js +3 -0
  15. package/dist/src/onboarding/create.js.map +1 -1
  16. package/dist/src/onboarding/init.d.ts +4 -0
  17. package/dist/src/onboarding/init.d.ts.map +1 -1
  18. package/dist/src/onboarding/init.js +6 -0
  19. package/dist/src/onboarding/init.js.map +1 -1
  20. package/dist/src/profiles/profile-schema.d.ts +8 -0
  21. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  22. package/dist/src/profiles/profile-schema.js +6 -0
  23. package/dist/src/profiles/profile-schema.js.map +1 -1
  24. package/dist/src/profiles/stack-profile.d.ts +4 -0
  25. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  26. package/dist/src/profiles/stack-profile.js +2 -0
  27. package/dist/src/profiles/stack-profile.js.map +1 -1
  28. package/os/compass/compass.module.ts +32 -16
  29. package/os/compass/handlers/compass-change-summary-handler.ts +12 -2
  30. package/os/compass/handlers/compass-game-extensions.test.ts +154 -0
  31. package/os/compass/handlers/compass-inventory-handler.ts +65 -0
  32. package/os/compass/handlers/compass-inventory.ts +56 -7
  33. package/package.json +3 -5
  34. package/profiles/godot-csharp.yaml +17 -0
  35. package/profiles/phaser-turborepo.yaml +14 -0
  36. package/skills/fo/fo-compass-annotate/reference/comment-styles.md +7 -1
  37. package/src/onboarding/create.ts +3 -0
  38. package/src/onboarding/init.ts +10 -0
  39. package/src/profiles/profile-schema.ts +10 -0
  40. package/src/profiles/stack-profile.ts +2 -0
  41. package/src/tests/create.test.ts +30 -0
@@ -14,15 +14,51 @@ for Compass source-file inventory.</purpose>
14
14
  <item>Post-refactor hardening: exclude src/templates generation inputs from authored Compass requirements.</item>
15
15
  <item>Post-refactor hardening: detect nested packages/os workspaces before deriving Compass layer and workspace name.</item>
16
16
  <item>RFC-0556: moved canonical implementation from @warpgogol/site-kernel to @warpgogol/forge for autonomous mode.</item>
17
+ <item>Game extensions: added .cs, .tscn, .tres, .gd to SOURCE_EXTENSIONS; createCompassInventoryEntries now reads forge.yaml compass.fileExtensions at runtime and merges with hardcoded set.</item>
17
18
  </CHANGE_SUMMARY>
18
19
  */
19
20
 
20
21
  import { readdir, readFile } from "node:fs/promises";
22
+ import { readFileSync, existsSync } from "node:fs";
21
23
  import { join, relative, resolve } from "node:path";
24
+ import { parse as parseYaml } from "yaml";
22
25
  import type { ForgeCommandInput } from "../../../src/types.ts";
23
26
  import { hasGeneratedMarker } from "../../../src/utils/generated-marker.ts";
24
27
 
25
- const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".astro", ".js", ".jsx", ".mjs", ".mts", ".css"]);
28
+ function loadCompassExtensionsFromForgeYaml(workspaceRoot: string): Set<string> {
29
+ const forgeYamlPath = join(workspaceRoot, "forge.yaml");
30
+ if (!existsSync(forgeYamlPath)) {
31
+ return new Set();
32
+ }
33
+ try {
34
+ const raw = readFileSync(forgeYamlPath, "utf8");
35
+ const parsed = parseYaml(raw) as Record<string, unknown> | null;
36
+ const bindings = parsed?.bindings as Record<string, unknown> | undefined;
37
+ const compass = bindings?.compass as Record<string, unknown> | undefined;
38
+ const extensions = compass?.fileExtensions;
39
+ if (Array.isArray(extensions)) {
40
+ return new Set(extensions.filter((e: unknown) => typeof e === "string"));
41
+ }
42
+ } catch {
43
+ // forge.yaml not parseable or missing compass section — fall back to hardcoded set
44
+ }
45
+ return new Set();
46
+ }
47
+
48
+ const SOURCE_EXTENSIONS = new Set([
49
+ ".ts",
50
+ ".tsx",
51
+ ".astro",
52
+ ".js",
53
+ ".jsx",
54
+ ".mjs",
55
+ ".mts",
56
+ ".css",
57
+ ".cs",
58
+ ".tscn",
59
+ ".tres",
60
+ ".gd",
61
+ ]);
26
62
  const IGNORED_DIRECTORY_NAMES = new Set([
27
63
  ".git",
28
64
  ".turbo",
@@ -113,16 +149,26 @@ function shouldIgnoreDirectory(name: string): boolean {
113
149
  return false;
114
150
  }
115
151
 
116
- function hasRelevantExtension(filePath: string): boolean {
152
+ function hasRelevantExtension(filePath: string, extraExtensions?: Set<string>): boolean {
117
153
  for (const extension of SOURCE_EXTENSIONS) {
118
154
  if (filePath.endsWith(extension)) {
119
155
  return true;
120
156
  }
121
157
  }
158
+ if (extraExtensions) {
159
+ for (const extension of extraExtensions) {
160
+ if (filePath.endsWith(extension)) {
161
+ return true;
162
+ }
163
+ }
164
+ }
122
165
  return false;
123
166
  }
124
167
 
125
- async function collectSourceFiles(targetPath: string): Promise<string[]> {
168
+ async function collectSourceFiles(
169
+ targetPath: string,
170
+ extraExtensions?: Set<string>,
171
+ ): Promise<string[]> {
126
172
  let stat;
127
173
  try {
128
174
  const { stat: fsStat } = await import("node:fs/promises");
@@ -132,7 +178,7 @@ async function collectSourceFiles(targetPath: string): Promise<string[]> {
132
178
  }
133
179
 
134
180
  if (stat.isFile()) {
135
- return hasRelevantExtension(targetPath) ? [targetPath] : [];
181
+ return hasRelevantExtension(targetPath, extraExtensions) ? [targetPath] : [];
136
182
  }
137
183
 
138
184
  let entries;
@@ -161,11 +207,11 @@ async function collectSourceFiles(targetPath: string): Promise<string[]> {
161
207
  continue;
162
208
  }
163
209
 
164
- files.push(...(await collectSourceFiles(absolutePath)));
210
+ files.push(...(await collectSourceFiles(absolutePath, extraExtensions)));
165
211
  continue;
166
212
  }
167
213
 
168
- if (entry.isFile() && hasRelevantExtension(absolutePath)) {
214
+ if (entry.isFile() && hasRelevantExtension(absolutePath, extraExtensions)) {
169
215
  files.push(absolutePath);
170
216
  }
171
217
  }
@@ -469,7 +515,10 @@ export async function createCompassInventoryEntries(
469
515
  scanRoot?: string,
470
516
  ): Promise<CompassInventoryEntry[]> {
471
517
  const roots = scanRoot ? [scanRoot] : resolveScanRoots(workspaceRoot, input);
472
- const files = (await Promise.all(roots.map((root) => collectSourceFiles(root)))).flat();
518
+ const extraExtensions = loadCompassExtensionsFromForgeYaml(workspaceRoot);
519
+ const files = (
520
+ await Promise.all(roots.map((root) => collectSourceFiles(root, extraExtensions)))
521
+ ).flat();
473
522
  const entries: CompassInventoryEntry[] = [];
474
523
 
475
524
  for (const filePath of files.sort()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpgogol/forge",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -170,12 +170,10 @@
170
170
  "yaml": "^2.9.0",
171
171
  "zod": "^4.4.3"
172
172
  },
173
- "optionalDependencies": {
174
- "@warpgogol/werkstatt-shared": "*",
175
- "@warpgogol/werkstatt": "*"
176
- },
177
173
  "devDependencies": {
178
174
  "@types/node": "^24.0.0",
175
+ "@warpgogol/werkstatt-shared": "*",
176
+ "@warpgogol/werkstatt": "1.4.0",
179
177
  "eslint": "^10.8.0",
180
178
  "fast-check": "^4.9.0",
181
179
  "typescript": "^6.0.3",
@@ -6,6 +6,16 @@ detect:
6
6
  - project.godot
7
7
  domain: game
8
8
  register: creative
9
+ compass:
10
+ fileExtensions:
11
+ - .cs
12
+ - .tscn
13
+ - .tres
14
+ - .gd
15
+ - .ts
16
+ - .tsx
17
+ - .js
18
+ - .mjs
9
19
  terminology:
10
20
  artifact: game
11
21
  artifactPlural: games
@@ -294,6 +304,9 @@ workspace:
294
304
  - name: Test
295
305
  run: pnpm test
296
306
 
307
+ - name: Compass header validation
308
+ run: pnpm exec werkstatt run compass.validate
309
+
297
310
  # Windows CI (optional — uncomment if Windows checks are needed):
298
311
  #
299
312
  # windows-ci:
@@ -677,9 +690,13 @@ workspace:
677
690
  #!/bin/sh
678
691
  # Platform-scope pre-commit guard
679
692
  # Runs werkstatt autonomy and plugin validation before commits
693
+ # Note: compass.validate reads all source files and may add a few seconds
694
+ # to commit time for large projects. Remove the line below if this is
695
+ # too slow during iterative development.
680
696
  echo "Pre-commit: running platform checks..."
681
697
  pnpm exec werkstatt run werkstatt.autonomy.validate || exit 1
682
698
  pnpm exec werkstatt run werkstatt.plugin.validate || exit 1
699
+ pnpm exec werkstatt run compass.validate || exit 1
683
700
  - path: systems-cache/.gitkeep
684
701
  content: |
685
702
  # Sternsystem cache directory (RFC-0790)
@@ -5,6 +5,13 @@ detect:
5
5
  anyOf:
6
6
  - phaser.config.*
7
7
  - package.json
8
+ compass:
9
+ fileExtensions:
10
+ - .ts
11
+ - .tsx
12
+ - .js
13
+ - .mjs
14
+ - .css
8
15
  workspace:
9
16
  dirs:
10
17
  - apps
@@ -123,6 +130,9 @@ workspace:
123
130
  - name: Test
124
131
  run: pnpm test
125
132
 
133
+ - name: Compass header validation
134
+ run: pnpm exec werkstatt run compass.validate
135
+
126
136
  # Windows CI (optional — uncomment if Windows checks are needed):
127
137
  #
128
138
  # windows-ci:
@@ -283,9 +293,13 @@ workspace:
283
293
  #!/bin/sh
284
294
  # Platform-scope pre-commit guard
285
295
  # Runs werkstatt autonomy and plugin validation before commits
296
+ # Note: compass.validate reads all source files and may add a few seconds
297
+ # to commit time for large projects. Remove the line below if this is
298
+ # too slow during iterative development.
286
299
  echo "Pre-commit: running platform checks..."
287
300
  pnpm exec werkstatt run werkstatt.autonomy.validate || exit 1
288
301
  pnpm exec werkstatt run werkstatt.plugin.validate || exit 1
302
+ pnpm exec werkstatt run compass.validate || exit 1
289
303
  - path: systems-cache/.gitkeep
290
304
  content: |
291
305
  # Sternsystem cache directory (RFC-0790)
@@ -12,6 +12,10 @@ Compass headers are embedded in source file comments. The comment syntax varies
12
12
  | `.js` | `/* ... */` block comment | `/* <MODULE_CONTRACT> ... </MODULE_CONTRACT> */` |
13
13
  | `.mjs` | `/* ... */` block comment | `/* <MODULE_CONTRACT> ... </MODULE_CONTRACT> */` |
14
14
  | `.css` | `/* ... */` block comment | `/* <MODULE_CONTRACT> ... </MODULE_CONTRACT> */` |
15
+ | `.cs` | `/* ... */` block comment | `/* <MODULE_CONTRACT> ... </MODULE_CONTRACT> */` |
16
+ | `.tscn` | `; ` line comment | `; <MODULE_CONTRACT>` (each line prefixed with `; `) |
17
+ | `.tres` | `; ` line comment | `; <MODULE_CONTRACT>` (each line prefixed with `; `) |
18
+ | `.gd` | `# ` line comment | `# <MODULE_CONTRACT>` (each line prefixed with `# `) |
15
19
  | `.yaml` | `# ` line comment | `# <MODULE_CONTRACT>` (each line prefixed with `# `) |
16
20
  | `.yml` | `# ` line comment | `# <MODULE_CONTRACT>` (each line prefixed with `# `) |
17
21
  | `.json` | Not supported | JSON files cannot carry comments; skip Compass headers |
@@ -20,6 +24,8 @@ Compass headers are embedded in source file comments. The comment syntax varies
20
24
  ## Rules
21
25
 
22
26
  - The XML-like tags (`<MODULE_CONTRACT>`, `<CHANGE_SUMMARY>`, `<item>`, etc.) are always preserved as-is inside the comment wrapper.
23
- - For `.yaml`/`.yml`, each line of the block is prefixed with `# `.
27
+ - For `.yaml`/`.yml` and `.gd`, each line of the block is prefixed with `# `.
28
+ - For `.tscn`/`.tres`, each line of the block is prefixed with `; ` (Godot INI-style comment).
29
+ - For `.cs`, use `/* ... */` block comments (same as TypeScript).
24
30
  - For `.json`, Compass headers are not applicable — skip these files.
25
31
  - The block is placed at the top of the file, after any license header but before code/imports.
@@ -87,6 +87,9 @@ function loadProfileDomainFields(profileId: string, forgeRoot: string): InitDoma
87
87
  fields.semanticBindings = semanticBindings;
88
88
  }
89
89
  }
90
+ if (profile.compass) {
91
+ fields.compass = profile.compass;
92
+ }
90
93
  return fields;
91
94
  } catch {
92
95
  return {};
@@ -52,6 +52,10 @@ export interface InitDomainFields {
52
52
  terminology?: Record<string, string>;
53
53
  semanticBindings?: Record<string, string | null>;
54
54
  profileId?: string;
55
+ compass?: {
56
+ fileExtensions?: string[];
57
+ testPatterns?: string[];
58
+ };
55
59
  }
56
60
 
57
61
  export function runInit(
@@ -150,6 +154,12 @@ export function runInit(
150
154
  }
151
155
  }
152
156
  }
157
+ if (domainFields?.compass && config.bindings) {
158
+ config.bindings.compass = {
159
+ ...(config.bindings.compass ?? {}),
160
+ ...domainFields.compass,
161
+ };
162
+ }
153
163
  const yamlContent = stringifyYaml(config);
154
164
  fs.writeFileSync(forgeYamlPath, yamlContent, "utf8");
155
165
  created.push("forge.yaml");
@@ -331,6 +331,12 @@ export const stackProfileDomainFieldsSchema = z.object({
331
331
  templates: z.array(profileTemplateSchema).optional(),
332
332
  scriptDir: z.string().min(1).optional(),
333
333
  rootAgentsMdTemplate: z.string().optional(),
334
+ compass: z
335
+ .object({
336
+ fileExtensions: z.array(z.string()).optional(),
337
+ testPatterns: z.array(z.string()).optional(),
338
+ })
339
+ .optional(),
334
340
  });
335
341
 
336
342
  export interface StackProfileDomainFields {
@@ -347,4 +353,8 @@ export interface StackProfileDomainFields {
347
353
  templates?: ProfileTemplate[];
348
354
  scriptDir?: string;
349
355
  rootAgentsMdTemplate?: string;
356
+ compass?: {
357
+ fileExtensions?: string[];
358
+ testPatterns?: string[];
359
+ };
350
360
  }
@@ -76,6 +76,8 @@ export const stackProfileSchema = z.object({
76
76
  scriptDir: stackProfileDomainFieldsSchema.shape.scriptDir,
77
77
  // Profile-driven root AGENTS.md template
78
78
  rootAgentsMdTemplate: stackProfileDomainFieldsSchema.shape.rootAgentsMdTemplate,
79
+ // Profile-driven Compass file extensions
80
+ compass: stackProfileDomainFieldsSchema.shape.compass,
79
81
  });
80
82
 
81
83
  export interface ProfileFile {
@@ -287,3 +287,33 @@ test("forge create --in-place derives name from folder name when --name omitted"
287
287
  const pkgJson = JSON.parse(await readFileAsync(join(namedDir, "package.json"), "utf8"));
288
288
  expect(pkgJson.name).toBe("my-derived-project");
289
289
  }, 30000);
290
+
291
+ test("forge create --profile godot-csharp writes compass.fileExtensions into forge.yaml", async () => {
292
+ const result = await runCreate(
293
+ { argv: [], flags: { "in-place": true, profile: "godot-csharp", name: "my-game" } },
294
+ makeContext(tempDir),
295
+ );
296
+ expect(result.exitCode).toBe(0);
297
+
298
+ const { readFile: readFileAsync } = await import("node:fs/promises");
299
+ const forgeYaml = await readFileAsync(join(tempDir, "forge.yaml"), "utf8");
300
+ expect(forgeYaml).toContain("compass:");
301
+ expect(forgeYaml).toContain("fileExtensions:");
302
+ expect(forgeYaml).toContain(".cs");
303
+ expect(forgeYaml).toContain(".tscn");
304
+ expect(forgeYaml).toContain(".tres");
305
+ expect(forgeYaml).toContain(".gd");
306
+ }, 30000);
307
+
308
+ test("forge create --profile phaser-turborepo writes compass.fileExtensions into forge.yaml", async () => {
309
+ const result = await runCreate(
310
+ { argv: [], flags: { "in-place": true, profile: "phaser-turborepo", name: "my-phaser-game" } },
311
+ makeContext(tempDir),
312
+ );
313
+ expect(result.exitCode).toBe(0);
314
+
315
+ const { readFile: readFileAsync } = await import("node:fs/promises");
316
+ const forgeYaml = await readFileAsync(join(tempDir, "forge.yaml"), "utf8");
317
+ expect(forgeYaml).toContain("compass:");
318
+ expect(forgeYaml).toContain("fileExtensions:");
319
+ }, 30000);