@warpgogol/forge 2.8.0 → 2.8.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 (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +54 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +221 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,163 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { runNamingConventionLint } from "./naming-convention.ts";
3
+ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import type { ForgeCommandInput, ForgeFlagValue, ForgeRuntimeContext } from "../../src/types.ts";
7
+
8
+ const noopLogger: ForgeRuntimeContext["logger"] = {
9
+ section() {},
10
+ info() {},
11
+ warn() {},
12
+ error() {},
13
+ success() {},
14
+ };
15
+
16
+ async function makeTempRepo(): Promise<string> {
17
+ const dir = await mkdtemp(join(tmpdir(), "forge-naming-"));
18
+ await mkdir(join(dir, "packages"));
19
+ await mkdir(join(dir, "docs"));
20
+ return dir;
21
+ }
22
+
23
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
24
+ return {
25
+ workspaceRoot,
26
+ logger: noopLogger,
27
+ dryRun: false,
28
+ outputFormat: "pretty",
29
+ };
30
+ }
31
+
32
+ function makeInput(flags: Record<string, ForgeFlagValue> = {}): ForgeCommandInput {
33
+ return { argv: [], flags };
34
+ }
35
+
36
+ describe("runNamingConventionLint", () => {
37
+ test("passes on kebab-case filenames", async () => {
38
+ const dir = await makeTempRepo();
39
+ try {
40
+ await writeFile(join(dir, "packages", "my-file.ts"), "");
41
+ await writeFile(join(dir, "docs", "my-doc.md"), "");
42
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
43
+ expect(result.exitCode).toBe(0);
44
+ expect(result.data?.violations).toBe(0);
45
+ } finally {
46
+ await rm(dir, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test("flags camelCase filenames", async () => {
51
+ const dir = await makeTempRepo();
52
+ try {
53
+ await writeFile(join(dir, "packages", "myFile.ts"), "");
54
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
55
+ expect(result.exitCode).toBe(1);
56
+ expect(result.data?.violations).toBeGreaterThan(0);
57
+ } finally {
58
+ await rm(dir, { recursive: true, force: true });
59
+ }
60
+ });
61
+
62
+ test("flags PascalCase filenames", async () => {
63
+ const dir = await makeTempRepo();
64
+ try {
65
+ await writeFile(join(dir, "packages", "MyFile.ts"), "");
66
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
67
+ expect(result.exitCode).toBe(1);
68
+ expect(result.data?.violations).toBeGreaterThan(0);
69
+ } finally {
70
+ await rm(dir, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test("flags underscore filenames", async () => {
75
+ const dir = await makeTempRepo();
76
+ try {
77
+ await writeFile(join(dir, "packages", "my_file.ts"), "");
78
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
79
+ expect(result.exitCode).toBe(1);
80
+ expect(result.data?.violations).toBeGreaterThan(0);
81
+ } finally {
82
+ await rm(dir, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ test("exempts dotfiles", async () => {
87
+ const dir = await makeTempRepo();
88
+ try {
89
+ await writeFile(join(dir, "packages", ".eslintrc"), "");
90
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
91
+ expect(result.exitCode).toBe(0);
92
+ } finally {
93
+ await rm(dir, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ test("exempts underscore-prefixed files", async () => {
98
+ const dir = await makeTempRepo();
99
+ try {
100
+ await writeFile(join(dir, "packages", "_shared.ts"), "");
101
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
102
+ expect(result.exitCode).toBe(0);
103
+ } finally {
104
+ await rm(dir, { recursive: true, force: true });
105
+ }
106
+ });
107
+
108
+ test("exempts ALLCAPS files", async () => {
109
+ const dir = await makeTempRepo();
110
+ try {
111
+ await writeFile(join(dir, "docs", "README.md"), "");
112
+ await writeFile(join(dir, "docs", "AGENTS.md"), "");
113
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
114
+ expect(result.exitCode).toBe(0);
115
+ } finally {
116
+ await rm(dir, { recursive: true, force: true });
117
+ }
118
+ });
119
+
120
+ test("exempts files with 'config' in the name", async () => {
121
+ const dir = await makeTempRepo();
122
+ try {
123
+ await writeFile(join(dir, "packages", "astro.config.mjs"), "");
124
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
125
+ expect(result.exitCode).toBe(0);
126
+ } finally {
127
+ await rm(dir, { recursive: true, force: true });
128
+ }
129
+ });
130
+
131
+ test("exempts files with 'module' in the name", async () => {
132
+ const dir = await makeTempRepo();
133
+ try {
134
+ await writeFile(join(dir, "packages", "check.module.ts"), "");
135
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
136
+ expect(result.exitCode).toBe(0);
137
+ } finally {
138
+ await rm(dir, { recursive: true, force: true });
139
+ }
140
+ });
141
+
142
+ test("exempts Dockerfile", async () => {
143
+ const dir = await makeTempRepo();
144
+ try {
145
+ await writeFile(join(dir, "packages", "Dockerfile"), "");
146
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
147
+ expect(result.exitCode).toBe(0);
148
+ } finally {
149
+ await rm(dir, { recursive: true, force: true });
150
+ }
151
+ });
152
+
153
+ test("reports unknown top-level directories", async () => {
154
+ const dir = await makeTempRepo();
155
+ try {
156
+ await mkdir(join(dir, "UnknownDir"));
157
+ const result = await runNamingConventionLint(makeInput(), makeContext(dir));
158
+ expect(result.data?.unknownTopLevelDirs).toContain("UnknownDir");
159
+ } finally {
160
+ await rm(dir, { recursive: true, force: true });
161
+ }
162
+ });
163
+ });
@@ -0,0 +1,378 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>naming.convention.lint — validates that every filename in registered repo roots
4
+ uses kebab-case (lowercase letters, digits, hyphens only), with documented exemptions.</purpose>
5
+ <non-goals>
6
+ <item>Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0303: split out of structure.ts (Phase 3 file-size split).</item>
11
+ <item>RFC-0360: extend scan roots from apps/ and packages/ to the whole repo.</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ import { basename, join, relative } from "node:path";
16
+ import { readFile } from "node:fs/promises";
17
+ import { readdirSync, type Dirent } from "node:fs";
18
+ import { collectFiles } from "../../src/utils/fs.ts";
19
+ import type {
20
+ ForgeCommandInput,
21
+ ForgeCommandResult,
22
+ ForgeRuntimeContext,
23
+ } from "../../src/types.ts";
24
+
25
+ // @ai-invariant The exception rules below are the canonical source for naming.convention.lint.
26
+ // Update naming-conventions.md (packages/site-kernel/docs/) when these rules change.
27
+
28
+ // RFC-0360: Registered recursive roots — scanned recursively for filename violations.
29
+ const NAMING_CONVENTION_RECURSIVE_ROOTS = [
30
+ "apps",
31
+ "packages",
32
+ "services",
33
+ "docs",
34
+ "integrations",
35
+ "onboarding",
36
+ "fleet",
37
+ "hooks",
38
+ "tools",
39
+ "scripts",
40
+ "systems",
41
+ "tsconfig",
42
+ ] as const;
43
+
44
+ // RFC-0360: Registered ephemeral roots — root name validated, contents skipped unless --include-ignored.
45
+ const NAMING_CONVENTION_EPHEMERAL_ROOTS = ["missions", "releases", "agents", ".werkstatt"] as const;
46
+
47
+ // RFC-0360: Tool/cache directories that are never scanned at any level.
48
+ const NAMING_CONVENTION_IGNORED_TOP_LEVEL = new Set([
49
+ "node_modules",
50
+ ".git",
51
+ ".turbo",
52
+ ".astro",
53
+ ".wrangler",
54
+ ".vscode",
55
+ ".idea",
56
+ "coverage",
57
+ ".changelog-system",
58
+ ".codex-runlogs",
59
+ ".agents",
60
+ ".claude",
61
+ ".github",
62
+ ".opencode",
63
+ ".windsurf",
64
+ ".cache",
65
+ "tmp",
66
+ ]);
67
+
68
+ // Tool-mandated filenames exempt from kebab-case (Docker, Caddy use these exact names).
69
+ const NAMING_CONVENTION_TOOL_MANDATED = new Set(["Dockerfile", "Caddyfile"]);
70
+ const NAMING_CONVENTION_IGNORED_DIRS = new Set([
71
+ "node_modules",
72
+ "dist",
73
+ ".git",
74
+ ".turbo",
75
+ ".astro",
76
+ ".wrangler",
77
+ ".vscode",
78
+ ".idea",
79
+ "coverage",
80
+ "spec",
81
+ "todo",
82
+ // Generated, gitignored media-artifact dirs under public/ — their filenames are owned by the
83
+ // generators, not authored: RFC-0204 responsive image variants (_img) and RFC-0210 video
84
+ // variants / HLS segments (_video, e.g. "1080p_000.ts", where ".ts" is MPEG-TS, not TypeScript).
85
+ "_img",
86
+ "_video",
87
+ // DB migration dirs follow the ordered tool convention "NNN_name.sql" (Supabase/Postgres,
88
+ // RFC-0186) — the numeric-underscore prefix is required for ordering and is not authored prose.
89
+ "migrations",
90
+ ]);
91
+ const NAMING_CONVENTION_EXEMPT_KEYWORDS = ["config", "module"] as const;
92
+
93
+ // Set of directory paths that are exempt from naming convention (generated files)
94
+ const NAMING_CONVENTION_EXEMPT_DIRS = new Set([
95
+ "components/icons/gen", // Generated icon components — naming controlled by generator
96
+ ]);
97
+
98
+ /**
99
+ * Returns true when a filename is exempt from kebab-case enforcement.
100
+ *
101
+ * Exemptions (see naming-conventions.md for the canonical description):
102
+ * 1. File name starts with a dot (.env, .gitignore, ...).
103
+ * 2. File name starts with an underscore (_shared.ts, _headers, etc.).
104
+ * 3. File name is ALLCAPS (AGENTS.md, README.md, LICENSE, etc.).
105
+ * 4. File name (full, including extension) contains "config" or "module".
106
+ * 5. File is in an exempt directory (components/icons/gen — generated files).
107
+ * 6. Base icon component (LordIconBase.astro) — used by generated icon components.
108
+ */
109
+ function isNamingExempt(fileName: string, filePath?: string): boolean {
110
+ if (fileName.startsWith(".")) return true;
111
+ if (fileName.startsWith("_")) return true;
112
+ if (NAMING_CONVENTION_TOOL_MANDATED.has(fileName)) return true;
113
+ // SHOUTY_SNAKE_CASE files are exempt (AGENTS.md, README.md, LICENSE,
114
+ // ICONS_GENERATE.md, etc.). RFC-0132 extension: the check uses the FIRST
115
+ // stem segment so multi-segment names like AGENTS.template.md and
116
+ // SCHEMA.draft.md inherit the exemption from their canonical first segment
117
+ // — this is intentional for template files whose generated artifact carries
118
+ // the canonical name (AGENTS.template.md → AGENTS.md after agents.generate).
119
+ const firstDotIndex = fileName.indexOf(".");
120
+ const firstStem = firstDotIndex > 0 ? fileName.slice(0, firstDotIndex) : fileName;
121
+ if (firstStem.length > 0 && /^[A-Z]+(_[A-Z]+)*$/.test(firstStem)) return true;
122
+ for (const keyword of NAMING_CONVENTION_EXEMPT_KEYWORDS) {
123
+ if (fileName.includes(keyword)) return true;
124
+ }
125
+ // Check if file is in an exempt directory
126
+ if (filePath) {
127
+ for (const exemptDir of NAMING_CONVENTION_EXEMPT_DIRS) {
128
+ if (filePath.includes(exemptDir)) return true;
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+
134
+ /**
135
+ * Returns true when a filename violates the kebab-case convention.
136
+ * Violations: underscores OR mixed-case (PascalCase, camelCase).
137
+ * Note: ALLCAPS files are exempt, not violations.
138
+ * Only called when the file is not already exempt.
139
+ */
140
+ function hasNamingViolation(fileName: string): boolean {
141
+ const dotIndex = fileName.lastIndexOf(".");
142
+ const stem = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
143
+ // kebab-case allows only lowercase letters, digits, and hyphens
144
+ // Violations: underscores OR mixed-case (contains both upper and lower)
145
+ if (stem.includes("_")) return true;
146
+ // Mixed case: has at least one uppercase AND at least one lowercase
147
+ const hasUpper = /[A-Z]/.test(stem);
148
+ const hasLower = /[a-z]/.test(stem);
149
+ return hasUpper && hasLower;
150
+ }
151
+
152
+ /**
153
+ * Reads a gitignore-style file and returns a set of simple (no-wildcard) patterns.
154
+ * Patterns containing `*` or `?` are skipped — too complex without a full gitignore engine.
155
+ */
156
+ async function readSimpleIgnorePatterns(filePath: string): Promise<Set<string>> {
157
+ const patterns = new Set<string>();
158
+ try {
159
+ const content = await readFile(filePath, "utf8");
160
+ for (const rawLine of content.split("\n")) {
161
+ const line = rawLine.trim();
162
+ if (!line || line.startsWith("#")) continue;
163
+ if (line.includes("*") || line.includes("?")) continue;
164
+ patterns.add(line.replace(/^\//, "").replace(/\/$/, ""));
165
+ }
166
+ } catch {
167
+ // File absent — silently skip
168
+ }
169
+ return patterns;
170
+ }
171
+
172
+ function isSegmentIgnored(relPath: string, ignorePatterns: Set<string>): boolean {
173
+ for (const segment of relPath.split("/")) {
174
+ if (ignorePatterns.has(segment)) return true;
175
+ }
176
+ return false;
177
+ }
178
+
179
+ async function collectAllFilesForNaming(
180
+ dirPath: string,
181
+ ignorePatterns: Set<string>,
182
+ ): Promise<string[]> {
183
+ return collectFiles(dirPath, {
184
+ ignore: (name) =>
185
+ name.startsWith(".") ||
186
+ NAMING_CONVENTION_IGNORED_DIRS.has(name) ||
187
+ ignorePatterns.has(name) ||
188
+ ignorePatterns.has(`${name}/`),
189
+ });
190
+ }
191
+
192
+ interface NamingScanPlan {
193
+ recursiveRoots: string[];
194
+ ephemeralRoots: string[];
195
+ unknown: string[];
196
+ }
197
+
198
+ function isRegisteredOrIgnoredTopLevel(name: string): boolean {
199
+ return (
200
+ (NAMING_CONVENTION_RECURSIVE_ROOTS as readonly string[]).includes(name) ||
201
+ (NAMING_CONVENTION_EPHEMERAL_ROOTS as readonly string[]).includes(name) ||
202
+ NAMING_CONVENTION_IGNORED_TOP_LEVEL.has(name)
203
+ );
204
+ }
205
+
206
+ function resolveScanPlan(repoRoot: string): NamingScanPlan {
207
+ let entries: Dirent[];
208
+ try {
209
+ entries = readdirSync(repoRoot, { withFileTypes: true });
210
+ } catch {
211
+ return {
212
+ recursiveRoots: [...NAMING_CONVENTION_RECURSIVE_ROOTS],
213
+ ephemeralRoots: [...NAMING_CONVENTION_EPHEMERAL_ROOTS],
214
+ unknown: [],
215
+ };
216
+ }
217
+
218
+ const topLevelDirs = entries
219
+ .filter((e) => e.isDirectory() && !e.isSymbolicLink())
220
+ .map((e) => e.name);
221
+
222
+ const unknown = topLevelDirs.filter((name) => !isRegisteredOrIgnoredTopLevel(name));
223
+ const recursiveRoots = (NAMING_CONVENTION_RECURSIVE_ROOTS as readonly string[]).filter((name) =>
224
+ topLevelDirs.includes(name),
225
+ );
226
+ const ephemeralRoots = (NAMING_CONVENTION_EPHEMERAL_ROOTS as readonly string[]).filter((name) =>
227
+ topLevelDirs.includes(name),
228
+ );
229
+
230
+ return { recursiveRoots, ephemeralRoots, unknown };
231
+ }
232
+
233
+ /**
234
+ * Validates that all filenames in registered repo roots use kebab-case.
235
+ *
236
+ * Rule: filenames must use only lowercase letters, digits, and hyphens.
237
+ * Violations: underscores, uppercase letters (PascalCase, camelCase, ALLCAPS).
238
+ *
239
+ * Exceptions (not flagged):
240
+ * - Files starting with a dot (.env, .gitignore)
241
+ * - Files starting with an underscore (_shared.ts, _headers, _redirects, _routes.json)
242
+ * - Tool-mandated filenames (Dockerfile, Caddyfile)
243
+ * - Files containing "config" or "module" in their name (astro.config.mjs, check.module.ts)
244
+ * - Files in directories starting with a dot (.git, .astro, .vscode)
245
+ * - Files excluded by .gitignore or .windsurfignore (simple non-wildcard patterns only)
246
+ *
247
+ * See packages/site-kernel/docs/naming-conventions.md — "Global Rule" section.
248
+ */
249
+ export async function runNamingConventionLint(
250
+ input: ForgeCommandInput,
251
+ context: ForgeRuntimeContext,
252
+ ): Promise<
253
+ ForgeCommandResult<{
254
+ violations: number;
255
+ checkedFiles: number;
256
+ scannedRoots?: string[];
257
+ ephemeralRootsSkipped?: string[];
258
+ unknownTopLevelDirs?: string[];
259
+ }>
260
+ > {
261
+ const workspaceRoot = context.workspaceRoot;
262
+ const includeIgnored = input.flags["include-ignored"] === true;
263
+
264
+ const gitignorePatterns = await readSimpleIgnorePatterns(join(workspaceRoot, ".gitignore"));
265
+ const windsurfignorePatterns = await readSimpleIgnorePatterns(
266
+ join(workspaceRoot, ".windsurfignore"),
267
+ );
268
+ const ignorePatterns = new Set([...gitignorePatterns, ...windsurfignorePatterns]);
269
+
270
+ let violations = 0;
271
+ let checkedFiles = 0;
272
+
273
+ // App-scoped mode: scan only the app directory
274
+ if (context.site) {
275
+ const files = await collectAllFilesForNaming(context.site.directory, ignorePatterns);
276
+ for (const filePath of files) {
277
+ const fileName = basename(filePath);
278
+ const relPath = relative(workspaceRoot, filePath).replace(/\\/g, "/");
279
+ if (isNamingExempt(fileName, relPath)) continue;
280
+ if (isSegmentIgnored(relPath, ignorePatterns)) continue;
281
+ checkedFiles += 1;
282
+ if (hasNamingViolation(fileName)) {
283
+ context.logger.error(
284
+ `${relPath}: "${fileName}" violates kebab-case — use only lowercase letters, digits, and hyphens`,
285
+ );
286
+ violations += 1;
287
+ }
288
+ }
289
+ return {
290
+ data: { violations, checkedFiles },
291
+ exitCode: violations > 0 ? 1 : 0,
292
+ summary:
293
+ violations > 0
294
+ ? undefined
295
+ : `[naming.convention] OK (${checkedFiles} non-exempt files checked)`,
296
+ };
297
+ }
298
+
299
+ // Workspace-scoped mode: resolve scan plan from filesystem
300
+ const scanPlan = resolveScanPlan(workspaceRoot);
301
+
302
+ // Report unknown top-level directories
303
+ for (const unknownDir of scanPlan.unknown) {
304
+ context.logger.error(
305
+ `[naming.convention.lint] unknown top-level directory '${unknownDir}'; register it or remove it`,
306
+ );
307
+ violations += 1;
308
+ }
309
+
310
+ // Validate ephemeral root names (kebab-case check on the root name itself)
311
+ for (const ephemeral of scanPlan.ephemeralRoots) {
312
+ const name = ephemeral.startsWith(".") ? ephemeral.slice(1) : ephemeral;
313
+ if (name && !name.includes("_") && !(/[A-Z]/.test(name) && /[a-z]/.test(name))) {
314
+ // ok
315
+ } else if (name && (name.includes("_") || (/[A-Z]/.test(name) && /[a-z]/.test(name)))) {
316
+ context.logger.error(
317
+ `[naming.convention.lint] top-level directory '${ephemeral}' violates naming convention`,
318
+ );
319
+ violations += 1;
320
+ }
321
+ }
322
+
323
+ // Scan recursive roots
324
+ for (const rootName of scanPlan.recursiveRoots) {
325
+ const scanDir = join(workspaceRoot, rootName);
326
+ const files = await collectAllFilesForNaming(scanDir, ignorePatterns);
327
+
328
+ for (const filePath of files) {
329
+ const fileName = basename(filePath);
330
+ const relPath = relative(workspaceRoot, filePath).replace(/\\/g, "/");
331
+ if (isNamingExempt(fileName, relPath)) continue;
332
+ if (isSegmentIgnored(relPath, ignorePatterns)) continue;
333
+ checkedFiles += 1;
334
+ if (hasNamingViolation(fileName)) {
335
+ context.logger.error(
336
+ `${relPath}: "${fileName}" violates kebab-case — use only lowercase letters, digits, and hyphens`,
337
+ );
338
+ violations += 1;
339
+ }
340
+ }
341
+ }
342
+
343
+ // Scan ephemeral root contents only when --include-ignored is passed
344
+ if (includeIgnored) {
345
+ for (const rootName of scanPlan.ephemeralRoots) {
346
+ const scanDir = join(workspaceRoot, rootName);
347
+ const files = await collectAllFilesForNaming(scanDir, ignorePatterns);
348
+ for (const filePath of files) {
349
+ const fileName = basename(filePath);
350
+ const relPath = relative(workspaceRoot, filePath).replace(/\\/g, "/");
351
+ if (isNamingExempt(fileName, relPath)) continue;
352
+ if (isSegmentIgnored(relPath, ignorePatterns)) continue;
353
+ checkedFiles += 1;
354
+ if (hasNamingViolation(fileName)) {
355
+ context.logger.error(
356
+ `${relPath}: "${fileName}" violates kebab-case — use only lowercase letters, digits, and hyphens`,
357
+ );
358
+ violations += 1;
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ return {
365
+ data: {
366
+ violations,
367
+ checkedFiles,
368
+ scannedRoots: scanPlan.recursiveRoots,
369
+ ephemeralRootsSkipped: includeIgnored ? [] : scanPlan.ephemeralRoots,
370
+ unknownTopLevelDirs: scanPlan.unknown,
371
+ },
372
+ exitCode: violations > 0 ? 1 : 0,
373
+ summary:
374
+ violations > 0
375
+ ? undefined
376
+ : `[naming.convention] OK (${checkedFiles} non-exempt files checked)`,
377
+ };
378
+ }
@@ -0,0 +1,36 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Register the forge naming convention lint command with the Site OS kernel registry.</purpose>
4
+ <non-goals>
5
+ <item>Do not register project-specific naming commands (naming.pages.lint, etc.) — those stay in site-kernel-checks.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0374: initial forgeNamingModule registering naming.convention.lint.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import type { ForgeModule } from "../../src/forge-module.ts";
14
+
15
+ export const forgeNamingModule: ForgeModule = {
16
+ name: "forge-naming",
17
+ version: "0.1.0",
18
+ async register(registry) {
19
+ const { runNamingConventionLint } = await import("./naming-convention.ts");
20
+ registry.registerCommand({
21
+ name: "naming.convention.lint",
22
+ description:
23
+ "Validate all filenames use kebab-case (no underscores) across registered workspace roots.",
24
+ scope: "workspace",
25
+ supportsAllSites: true,
26
+ flags: {
27
+ "include-ignored": {
28
+ kind: "boolean",
29
+ description: "Also scan files ignored by .gitignore or .windsurfignore.",
30
+ },
31
+ },
32
+ reads: ["packages/**/*.{ts,tsx}", "apps/**/*.{ts,tsx}", "services/**/*.{ts,tsx}"],
33
+ execute: runNamingConventionLint,
34
+ });
35
+ },
36
+ };
@@ -0,0 +1,10 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Re-export forgeNotesModule and note validator handlers.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0808: initial notes module index.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ export { forgeNotesModule } from "./notes.module.ts";
@@ -0,0 +1,109 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Register note validation commands (note.link.validate, note.frontmatter.validate, note.orphan.detect) with the forge kernel registry.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement handler logic here — delegate to src/validators/.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0808: initial forgeNotesModule registering 3 note validation commands.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import type { ForgeModule } from "../../src/forge-module.ts";
14
+ import type {
15
+ ForgeCommandInput,
16
+ ForgeCommandResult,
17
+ ForgeRuntimeContext,
18
+ } from "../../src/types.ts";
19
+
20
+ export const forgeNotesModule: ForgeModule = {
21
+ name: "forge-notes",
22
+ version: "0.1.0",
23
+ async register(registry) {
24
+ const { runNoteLinkValidate } = await import("../../src/validators/note-link-validate.ts");
25
+ const { runNoteFrontmatterValidate } =
26
+ await import("../../src/validators/note-frontmatter-validate.ts");
27
+ const { runNoteOrphanDetect } = await import("../../src/validators/note-orphan-detect.ts");
28
+
29
+ const noteLinkValidateWrapper = async (
30
+ input: ForgeCommandInput,
31
+ context: ForgeRuntimeContext,
32
+ ): Promise<ForgeCommandResult> => {
33
+ return runNoteLinkValidate(input, context);
34
+ };
35
+
36
+ const noteFrontmatterValidateWrapper = async (
37
+ input: ForgeCommandInput,
38
+ context: ForgeRuntimeContext,
39
+ ): Promise<ForgeCommandResult> => {
40
+ return runNoteFrontmatterValidate(input, context);
41
+ };
42
+
43
+ const noteOrphanDetectWrapper = async (
44
+ input: ForgeCommandInput,
45
+ context: ForgeRuntimeContext,
46
+ ): Promise<ForgeCommandResult> => {
47
+ return runNoteOrphanDetect(input, context);
48
+ };
49
+
50
+ registry.registerCommand({
51
+ name: "note.link.validate",
52
+ description:
53
+ "Validate wikilink integrity across a markdown note vault. Scans [[wikilinks]] and resolves each against the note graph.",
54
+ scope: "workspace",
55
+ supportsAllSites: false,
56
+ flags: {
57
+ "vault-dir": {
58
+ kind: "string",
59
+ description: "Vault directory relative to workspace root (default: vault).",
60
+ },
61
+ path: {
62
+ kind: "string",
63
+ description: "Subdirectory within the vault to scope the scan to.",
64
+ },
65
+ },
66
+ reads: ["vault/**/*.md"],
67
+ cacheable: false,
68
+ execute: noteLinkValidateWrapper,
69
+ });
70
+
71
+ registry.registerCommand({
72
+ name: "note.frontmatter.validate",
73
+ description:
74
+ "Validate frontmatter consistency across a markdown note vault. Checks for required fields in YAML frontmatter.",
75
+ scope: "workspace",
76
+ supportsAllSites: false,
77
+ flags: {
78
+ "vault-dir": {
79
+ kind: "string",
80
+ description: "Vault directory relative to workspace root (default: vault).",
81
+ },
82
+ fields: {
83
+ kind: "string",
84
+ description: "Comma-separated list of required fields (default: title).",
85
+ },
86
+ },
87
+ reads: ["vault/**/*.md"],
88
+ cacheable: false,
89
+ execute: noteFrontmatterValidateWrapper,
90
+ });
91
+
92
+ registry.registerCommand({
93
+ name: "note.orphan.detect",
94
+ description:
95
+ "Detect orphan notes in a markdown note vault — notes with zero inbound wikilinks. Always exits zero (warnings, not errors).",
96
+ scope: "workspace",
97
+ supportsAllSites: false,
98
+ flags: {
99
+ "vault-dir": {
100
+ kind: "string",
101
+ description: "Vault directory relative to workspace root (default: vault).",
102
+ },
103
+ },
104
+ reads: ["vault/**/*.md"],
105
+ cacheable: false,
106
+ execute: noteOrphanDetectWrapper,
107
+ });
108
+ },
109
+ };