cluaupp 1.5.0 → 1.5.1

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 (323) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +10 -5
  3. package/api/CHANGELOG.md +3 -0
  4. package/api/chat.js +74 -0
  5. package/api/fixtures/roblox/README.md +6 -0
  6. package/api/fixtures/roblox/aux/sample.d.luau +6 -0
  7. package/api/fixtures/roblox/mini/Mini-API-Dump.json +72 -0
  8. package/api/generated/bench-latest.json +39 -0
  9. package/api/generated/capability-profile.json +76 -0
  10. package/api/generated/lsp-index.json +1 -0
  11. package/api/generated/luau/classes.d.luau +11221 -0
  12. package/api/generated/luau/datatypes.d.luau +327 -0
  13. package/api/generated/luau/enums.d.luau +4918 -0
  14. package/api/generated/luau/globals.d.luau +12 -0
  15. package/api/manifests/roblox-target.manifest.json +22 -0
  16. package/api/normalized/roblox-target.profile.json +237461 -0
  17. package/api/overrides/classes.json +8 -0
  18. package/api/overrides/compatibility.json +9 -0
  19. package/api/overrides/datatypes.json +1579 -0
  20. package/api/overrides/enums.json +8 -0
  21. package/api/overrides/globals.json +8 -0
  22. package/api/overrides/type-mappings.json +9 -0
  23. package/api/roblox-api.lock.json +9 -0
  24. package/data/Mini-API-Dump.json +1 -0
  25. package/editors/vscode/README.md +12 -10
  26. package/editors/vscode/extension.js +82 -0
  27. package/editors/vscode/icons/axiom.svg +4 -0
  28. package/editors/vscode/icons/bloom.svg +5 -0
  29. package/editors/vscode/icons/helm.svg +4 -0
  30. package/editors/vscode/icons/hive.svg +5 -0
  31. package/editors/vscode/icons/mint.svg +4 -0
  32. package/editors/vscode/icons/shift.svg +6 -0
  33. package/editors/vscode/language-configuration-schema.json +24 -0
  34. package/editors/vscode/package.json +172 -12
  35. package/editors/vscode/snippets/axiom.json +12 -0
  36. package/editors/vscode/snippets/bloom.json +12 -0
  37. package/editors/vscode/snippets/helm.json +12 -0
  38. package/editors/vscode/snippets/hive.json +12 -0
  39. package/editors/vscode/snippets/mint.json +12 -0
  40. package/editors/vscode/snippets/shift.json +17 -0
  41. package/editors/vscode/syntaxes/axiom.tmLanguage.json +5 -0
  42. package/editors/vscode/syntaxes/bloom.tmLanguage.json +5 -0
  43. package/editors/vscode/syntaxes/helm.tmLanguage.json +5 -0
  44. package/editors/vscode/syntaxes/hive.tmLanguage.json +5 -0
  45. package/editors/vscode/syntaxes/mint.tmLanguage.json +5 -0
  46. package/editors/vscode/syntaxes/schema.tmLanguage.json +89 -0
  47. package/editors/vscode/syntaxes/shift.tmLanguage.json +5 -0
  48. package/examples/game/.cluaupp/compile-cache/ReplicatedStorage__Shared__Constants__Datas__TemplateData.clh.json +1 -0
  49. package/examples/game/.cluaupp/compile-cache/ServerScriptService__Boot__DataBoot.server.clpp.json +1 -0
  50. package/examples/game/.cluaupp/compile-cache/ServerScriptService__Configuration__ServerConfig.clp.json +1 -0
  51. package/examples/game/.cluaupp/compile-cache/ServerScriptService__Handlers__Guard.server.clpp.json +1 -0
  52. package/examples/game/.cluaupp/compile-cache/ServerScriptService__Handlers__PlayerHandler.server.clpp.json +1 -0
  53. package/examples/game/.cluaupp/compile-cache/StarterPlayer__StarterCharacterScripts__Character__CharacterReady.client.clpp.json +1 -0
  54. package/examples/game/.cluaupp/compile-cache/StarterPlayer__StarterPlayerScripts__Controllers__DataController.client.clpp.json +1 -0
  55. package/examples/game/.cluaupp/compile-cache/index.json +12 -0
  56. package/examples/game/.cluaupp/datamodel-profile.json +94 -0
  57. package/examples/game/.vscode/settings.json +12 -1
  58. package/examples/game/include/clpp/datatypes.clh +337 -373
  59. package/examples/game/include/clpp/generated/datamodel.clh +25 -0
  60. package/examples/game/include/clpp/generated/enums.clh +1 -1
  61. package/examples/game/include/clpp/generated/instances.clh +41135 -41064
  62. package/examples/game/include/clpp/libs/axiom.clh +1 -1
  63. package/examples/game/include/clpp/libs/math.clh +14 -0
  64. package/examples/game/include/clpp/libs/spring.clh +17 -0
  65. package/examples/game/include/clpp/libs/ward.clh +1 -0
  66. package/examples/game/include/clpp/roblox.clh +3 -4
  67. package/examples/game/libs/ArrayIndexer/init.luau +209 -0
  68. package/examples/game/libs/Axiom/init.luau +608 -0
  69. package/examples/game/libs/Bloom/init.luau +433 -0
  70. package/examples/game/libs/Coil/init.luau +215 -0
  71. package/examples/game/libs/Crest/init.luau +426 -0
  72. package/examples/game/libs/Echo/init.luau +417 -0
  73. package/examples/game/libs/Ember/init.luau +365 -0
  74. package/examples/game/libs/Flare/init.luau +717 -0
  75. package/examples/game/libs/Gleam/init.luau +729 -0
  76. package/examples/game/libs/Guide/init.luau +215 -0
  77. package/examples/game/libs/Helm/init.luau +377 -0
  78. package/examples/game/libs/Hive/init.luau +241 -0
  79. package/examples/game/libs/Keep/Client.luau +114 -0
  80. package/examples/game/libs/Keep/Data.luau +883 -0
  81. package/examples/game/libs/Keep/Engine.luau +211 -0
  82. package/examples/game/libs/Keep/Enum.luau +6 -0
  83. package/examples/game/libs/Keep/Enums.luau +10 -0
  84. package/examples/game/libs/Keep/Path.luau +236 -0
  85. package/examples/game/libs/Keep/Replicator.luau +298 -0
  86. package/examples/game/libs/Keep/Server.luau +251 -0
  87. package/examples/game/libs/Keep/Store.luau +652 -0
  88. package/examples/game/libs/Keep/Trade.luau +455 -0
  89. package/examples/game/libs/Keep/init.luau +205 -0
  90. package/examples/game/libs/Lens/init.luau +609 -0
  91. package/examples/game/libs/Mint/init.luau +279 -0
  92. package/examples/game/libs/Net/init.luau +156 -0
  93. package/examples/game/libs/Net/src/codec.luau +224 -0
  94. package/examples/game/libs/Occlude/init.luau +117 -0
  95. package/examples/game/libs/Pin/init.luau +309 -0
  96. package/examples/game/libs/Promise/LICENSE +21 -0
  97. package/examples/game/libs/Promise/_impl/LICENSE +21 -0
  98. package/examples/game/libs/Promise/_impl/init.lua +1991 -0
  99. package/examples/game/libs/Promise/init.lua +1990 -0
  100. package/examples/game/libs/Promise/init.luau +13 -0
  101. package/examples/game/libs/Promise/types.luau +46 -0
  102. package/examples/game/libs/Roster/init.luau +686 -0
  103. package/examples/game/libs/SOURCES.md +31 -0
  104. package/examples/game/libs/Shift/init.luau +238 -0
  105. package/examples/game/libs/Spark/init.luau +487 -0
  106. package/examples/game/libs/Stage/init.luau +178 -0
  107. package/examples/game/libs/Sweep/init.luau +259 -0
  108. package/examples/game/libs/Trace/init.luau +213 -0
  109. package/examples/game/libs/Ward/init.luau +211 -0
  110. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +29 -0
  111. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +13 -0
  112. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +83 -0
  113. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +13 -0
  114. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +18 -0
  115. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +13 -0
  116. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +14 -0
  117. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +13 -0
  118. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +15 -0
  119. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +13 -0
  120. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +50 -0
  121. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +13 -0
  122. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +12 -0
  123. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +13 -0
  124. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +16 -0
  125. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +13 -0
  126. package/examples/game/src/ReplicatedStorage/Shared/Net/Net.clh +26 -0
  127. package/examples/game/src/ServerScriptService/Boot/DataBoot.server.clpp +1 -1
  128. package/examples/game/src/ServerScriptService/Handlers/PlayerHandler.server.clpp +1 -1
  129. package/examples/game/stylua.toml +9 -0
  130. package/generated/api/build-profile.js +115 -0
  131. package/generated/api/cache.js +44 -0
  132. package/generated/api/coverage.js +32 -0
  133. package/generated/api/diff.js +125 -0
  134. package/generated/api/generators/clpp-headers.js +353 -0
  135. package/generated/api/generators/docs-reference.js +7 -0
  136. package/generated/api/generators/lsp-index.js +81 -0
  137. package/generated/api/generators/luau-defs.js +94 -0
  138. package/generated/api/ide.js +76 -0
  139. package/generated/api/identifier.js +47 -0
  140. package/generated/api/index.js +324 -0
  141. package/generated/api/loader.js +57 -0
  142. package/generated/api/merge.js +88 -0
  143. package/generated/api/model.js +5 -0
  144. package/generated/api/normalize.js +231 -0
  145. package/generated/api/overrides-validate.js +58 -0
  146. package/generated/api/perf.js +12 -0
  147. package/generated/api/registry.js +136 -0
  148. package/generated/api/release.js +44 -0
  149. package/generated/api/resolver.js +250 -0
  150. package/generated/api/sources/aux-types-diff.js +75 -0
  151. package/generated/api/sources/client-tracker.js +11 -0
  152. package/generated/api/sources/creator-docs.js +42 -0
  153. package/generated/api/sources/luau-types.js +34 -0
  154. package/generated/api/sources/mini-dump.js +17 -0
  155. package/generated/api/sources/studio-api.js +8 -0
  156. package/generated/api/sources/studio-apiv2.js +11 -0
  157. package/generated/api/sources/studio-full-api.js +8 -0
  158. package/generated/api/studio-smoke.js +80 -0
  159. package/generated/api/type-parse.js +49 -0
  160. package/generated/api/understander.js +51 -0
  161. package/generated/api/validate.js +135 -0
  162. package/generated/api-cli.js +421 -0
  163. package/generated/cli.js +16 -5
  164. package/generated/clpp/contract.js +3 -3
  165. package/generated/clpp/format-luau.js +236 -0
  166. package/generated/clpp/index.js +13 -1
  167. package/generated/clpp/modules.js +227 -0
  168. package/generated/clpp/postprocess.js +86 -5
  169. package/generated/clpp/rewrite-janitor.js +145 -0
  170. package/generated/clpp/runner.js +14 -2
  171. package/generated/compile.js +82 -0
  172. package/generated/flare/parse.js +12 -1
  173. package/generated/intellisense.js +57 -1
  174. package/generated/native/index.js +217 -16
  175. package/generated/target/authority-check.js +136 -0
  176. package/generated/target/authority-graph.js +74 -0
  177. package/generated/target/build-cache.js +112 -0
  178. package/generated/target/capabilities.js +89 -0
  179. package/generated/target/capability-profile.js +30 -0
  180. package/generated/target/component-contracts.js +60 -0
  181. package/generated/target/context-check.js +160 -0
  182. package/generated/target/datamodel-check.js +170 -0
  183. package/generated/target/datamodel-generate.js +74 -0
  184. package/generated/target/datamodel.js +233 -0
  185. package/generated/target/diagnostics.js +63 -0
  186. package/generated/target/doctor.js +152 -0
  187. package/generated/target/flare-version.js +58 -0
  188. package/generated/target/get-service.js +69 -0
  189. package/generated/target/lifetime-check.js +47 -0
  190. package/generated/target/luau-stamp.js +39 -0
  191. package/generated/target/optimizer-advise.js +88 -0
  192. package/generated/target/optimizer-apply.js +44 -0
  193. package/generated/target/optimizer-soa.js +254 -0
  194. package/generated/target/parallel-check.js +136 -0
  195. package/generated/target/pgo.js +148 -0
  196. package/generated/target/project-docs.js +69 -0
  197. package/generated/target/project-graph.js +71 -0
  198. package/generated/target/roblox.js +63 -0
  199. package/generated/target/security-check.js +41 -0
  200. package/generated/target/source-map.js +25 -0
  201. package/generated/target/studio-bridge.js +112 -0
  202. package/generated/utils/project.js +178 -8
  203. package/generated/utils/rojo-mapper.js +11 -3
  204. package/include/clpp/datatypes.clh +337 -373
  205. package/include/clpp/generated/enums.clh +1 -1
  206. package/include/clpp/generated/instances.clh +41135 -41064
  207. package/include/clpp/libs/axiom.clh +1 -1
  208. package/include/clpp/libs/hive.clh +1 -0
  209. package/include/clpp/libs/keep.clh +2 -0
  210. package/include/clpp/libs/math.clh +14 -0
  211. package/include/clpp/libs/spring.clh +17 -0
  212. package/include/clpp/libs/ward.clh +1 -0
  213. package/include/clpp/roblox.clh +3 -4
  214. package/package.json +10 -3
  215. package/plugins/CluauppNav/Plugin.luau +207 -0
  216. package/plugins/CluauppNav/ProfileLogger.luau +27 -0
  217. package/plugins/CluauppNav/README.md +24 -0
  218. package/runtime/Axiom/init.luau +10 -7
  219. package/runtime/Coil/init.luau +8 -2
  220. package/runtime/Crest/init.luau +1 -0
  221. package/runtime/Flare/init.luau +2 -1
  222. package/runtime/Gleam/init.luau +15 -4
  223. package/runtime/Hive/init.luau +36 -14
  224. package/runtime/Pin/init.luau +5 -2
  225. package/runtime/Roster/init.luau +2 -0
  226. package/runtime/Spark/init.luau +3 -0
  227. package/runtime/Sweep/init.luau +2 -0
  228. package/runtime/Trace/init.luau +3 -0
  229. package/runtime/Ward/init.luau +38 -5
  230. package/scripts/postinstall-editor.js +17 -0
  231. package/skills/README.md +13 -0
  232. package/skills/clpp-hive-flare/SKILL.md +79 -0
  233. package/skills/clpp-language/SKILL.md +123 -0
  234. package/skills/clpp-language/compiler.md +89 -0
  235. package/skills/clpp-style/SKILL.md +95 -0
  236. package/src/api/build-profile.ts +138 -0
  237. package/src/api/cache.ts +45 -0
  238. package/src/api/coverage.ts +46 -0
  239. package/src/api/diff.ts +143 -0
  240. package/src/api/generators/clpp-headers.ts +377 -0
  241. package/src/api/generators/docs-reference.ts +11 -0
  242. package/src/api/generators/lsp-index.ts +106 -0
  243. package/src/api/generators/luau-defs.ts +98 -0
  244. package/src/api/ide.ts +87 -0
  245. package/src/api/identifier.ts +47 -0
  246. package/src/api/index.ts +269 -0
  247. package/src/api/loader.ts +60 -0
  248. package/src/api/merge.ts +126 -0
  249. package/src/api/model.ts +283 -0
  250. package/src/api/normalize.ts +257 -0
  251. package/src/api/overrides-validate.ts +58 -0
  252. package/src/api/perf.ts +15 -0
  253. package/src/api/registry.ts +162 -0
  254. package/src/api/release.ts +47 -0
  255. package/src/api/resolver.ts +255 -0
  256. package/src/api/sources/aux-types-diff.ts +81 -0
  257. package/src/api/sources/client-tracker.ts +10 -0
  258. package/src/api/sources/creator-docs.ts +37 -0
  259. package/src/api/sources/luau-types.ts +30 -0
  260. package/src/api/sources/mini-dump.ts +50 -0
  261. package/src/api/sources/studio-api.ts +7 -0
  262. package/src/api/sources/studio-apiv2.ts +10 -0
  263. package/src/api/sources/studio-full-api.ts +7 -0
  264. package/src/api/studio-smoke.ts +82 -0
  265. package/src/api/type-parse.ts +48 -0
  266. package/src/api/understander.ts +72 -0
  267. package/src/api/validate.ts +160 -0
  268. package/src/api-cli.ts +493 -0
  269. package/src/api.generated.js +37 -22
  270. package/src/cli.ts +21 -6
  271. package/src/clpp/contract.ts +34 -3
  272. package/src/clpp/format-luau.ts +262 -0
  273. package/src/clpp/index.ts +13 -2
  274. package/src/clpp/modules.ts +252 -0
  275. package/src/clpp/postprocess.ts +100 -5
  276. package/src/clpp/rewrite-janitor.ts +156 -0
  277. package/src/clpp/runner.ts +14 -2
  278. package/src/compile.ts +87 -0
  279. package/src/flare/parse.ts +14 -1
  280. package/src/flare/types.ts +2 -0
  281. package/src/intellisense.ts +58 -1
  282. package/src/native/index.ts +225 -16
  283. package/src/target/authority-check.ts +162 -0
  284. package/src/target/authority-graph.ts +80 -0
  285. package/src/target/build-cache.ts +129 -0
  286. package/src/target/capabilities.ts +115 -0
  287. package/src/target/capability-profile.ts +25 -0
  288. package/src/target/component-contracts.ts +64 -0
  289. package/src/target/context-check.ts +177 -0
  290. package/src/target/datamodel-check.ts +188 -0
  291. package/src/target/datamodel-generate.ts +78 -0
  292. package/src/target/datamodel.ts +261 -0
  293. package/src/target/diagnostics.ts +67 -0
  294. package/src/target/doctor.ts +171 -0
  295. package/src/target/flare-version.ts +61 -0
  296. package/src/target/get-service.ts +85 -0
  297. package/src/target/lifetime-check.ts +45 -0
  298. package/src/target/luau-stamp.ts +33 -0
  299. package/src/target/optimizer-advise.ts +93 -0
  300. package/src/target/optimizer-apply.ts +63 -0
  301. package/src/target/optimizer-soa.ts +270 -0
  302. package/src/target/parallel-check.ts +135 -0
  303. package/src/target/pgo.ts +157 -0
  304. package/src/target/project-docs.ts +68 -0
  305. package/src/target/project-graph.ts +75 -0
  306. package/src/target/roblox.ts +78 -0
  307. package/src/target/security-check.ts +40 -0
  308. package/src/target/source-map.ts +36 -0
  309. package/src/target/studio-bridge.ts +140 -0
  310. package/src/types.ts +14 -0
  311. package/src/utils/project.ts +183 -9
  312. package/src/utils/rojo-mapper.ts +12 -3
  313. package/stylua.toml +9 -0
  314. package/templates/game/.vscode/settings.json +7 -1
  315. package/templates/game/include/clpp/datatypes.clh +337 -373
  316. package/templates/game/include/clpp/generated/enums.clh +1 -1
  317. package/templates/game/include/clpp/generated/instances.clh +41135 -41064
  318. package/templates/game/include/clpp/libs/axiom.clh +1 -1
  319. package/templates/game/include/clpp/libs/ward.clh +1 -0
  320. package/templates/game/include/clpp/roblox.clh +3 -4
  321. package/templates/game/src/ServerScriptService/Boot/DataBoot.server.clpp +1 -1
  322. package/templates/game/src/ServerScriptService/Handlers/PlayerHandler.server.clpp +1 -1
  323. package/templates/game/stylua.toml +9 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Typed DataModel v1 — Rojo default.project.json → instance graph.
3
+ * Used for path validation at compile time and generated IntelliSense headers.
4
+ */
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { parse } from "comment-json";
9
+
10
+ export const DATAMODEL_SCHEMA_VERSION = 1;
11
+
12
+ export interface DatamodelNode {
13
+ /** Roblox instance name (tree key). */
14
+ name: string;
15
+ /** Dot path from game, e.g. ReplicatedStorage.Shared (DataModel root is "DataModel"). */
16
+ path: string;
17
+ className: string;
18
+ /** Project-relative fs path when backed by $path. */
19
+ fsPath?: string;
20
+ children: Record<string, DatamodelNode>;
21
+ }
22
+
23
+ export interface DatamodelProfile {
24
+ schemaVersion: number;
25
+ projectName: string;
26
+ projectJson: string;
27
+ root: DatamodelNode;
28
+ /** Flat index path → node */
29
+ byPath: Record<string, DatamodelNode>;
30
+ generatedAt: string;
31
+ }
32
+
33
+ function asRecord(value: unknown): Record<string, unknown> | null {
34
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
35
+ return null;
36
+ }
37
+ return value as Record<string, unknown>;
38
+ }
39
+
40
+ function posix(p: string): string {
41
+ return p.replace(/\\/g, "/");
42
+ }
43
+
44
+ function childPath(parentPath: string, name: string): string {
45
+ if (!parentPath || parentPath === "DataModel") {
46
+ return name;
47
+ }
48
+ return `${parentPath}.${name}`;
49
+ }
50
+
51
+ function readMetaClassName(dir: string): string | undefined {
52
+ for (const metaName of ["init.meta.json", "init.meta.jsonc"]) {
53
+ const file = path.join(dir, metaName);
54
+ if (!fs.existsSync(file)) {
55
+ continue;
56
+ }
57
+ try {
58
+ const meta = asRecord(parse(fs.readFileSync(file, "utf8")));
59
+ if (meta && typeof meta.className === "string") {
60
+ return meta.className;
61
+ }
62
+ } catch {
63
+ /* ignore */
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+
69
+ function classFromFileName(fileName: string): { name: string; className: string } | null {
70
+ const lower = fileName.toLowerCase();
71
+ if (lower === "init.meta.json" || lower === "init.meta.jsonc") {
72
+ return null;
73
+ }
74
+ if (/\.server\.(luau|lua|clpp)$/i.test(fileName)) {
75
+ return { name: fileName.replace(/\.server\.(luau|lua|clpp)$/i, ""), className: "Script" };
76
+ }
77
+ if (/\.client\.(luau|lua|clpp)$/i.test(fileName)) {
78
+ return { name: fileName.replace(/\.client\.(luau|lua|clpp)$/i, ""), className: "LocalScript" };
79
+ }
80
+ if (/\.plugin\.(luau|lua|clpp)$/i.test(fileName)) {
81
+ return { name: fileName.replace(/\.plugin\.(luau|lua|clpp)$/i, ""), className: "Script" };
82
+ }
83
+ if (/^init\.(luau|lua|clpp|clp)$/i.test(fileName)) {
84
+ return null;
85
+ }
86
+ if (/\.(luau|lua|clpp|clp|clh)$/i.test(fileName)) {
87
+ return {
88
+ name: fileName.replace(/\.(luau|lua|clpp|clp|clh)$/i, ""),
89
+ className: "ModuleScript",
90
+ };
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function scanFsChildren(absDir: string, parentPath: string, projectRoot: string): Record<string, DatamodelNode> {
96
+ const children: Record<string, DatamodelNode> = {};
97
+ if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
98
+ return children;
99
+ }
100
+ let entries: fs.Dirent[];
101
+ try {
102
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
103
+ } catch {
104
+ return children;
105
+ }
106
+ for (const entry of entries) {
107
+ if (entry.name.startsWith(".")) {
108
+ continue;
109
+ }
110
+ const full = path.join(absDir, entry.name);
111
+ if (entry.isDirectory()) {
112
+ const className = readMetaClassName(full) || "Folder";
113
+ const nodePath = childPath(parentPath, entry.name);
114
+ children[entry.name] = {
115
+ name: entry.name,
116
+ path: nodePath,
117
+ className,
118
+ fsPath: posix(path.relative(projectRoot, full) || "."),
119
+ children: scanFsChildren(full, nodePath, projectRoot),
120
+ };
121
+ continue;
122
+ }
123
+ if (!entry.isFile()) {
124
+ continue;
125
+ }
126
+ const mapped = classFromFileName(entry.name);
127
+ if (!mapped || !mapped.name) {
128
+ continue;
129
+ }
130
+ const nodePath = childPath(parentPath, mapped.name);
131
+ children[mapped.name] = {
132
+ name: mapped.name,
133
+ path: nodePath,
134
+ className: mapped.className,
135
+ fsPath: posix(path.relative(projectRoot, full)),
136
+ children: {},
137
+ };
138
+ }
139
+ return children;
140
+ }
141
+
142
+ function walkNamedNode(node: unknown, name: string, parentPath: string, projectRoot: string): DatamodelNode {
143
+ const record = asRecord(node) || {};
144
+ const nodePath = childPath(parentPath, name);
145
+ const className = typeof record.$className === "string" ? record.$className : "Folder";
146
+ const fsRel = typeof record.$path === "string" ? record.$path : undefined;
147
+ const absFs = fsRel ? path.resolve(projectRoot, fsRel) : undefined;
148
+
149
+ const children: Record<string, DatamodelNode> = {};
150
+ for (const [key, child] of Object.entries(record)) {
151
+ if (key.startsWith("$")) {
152
+ continue;
153
+ }
154
+ children[key] = walkNamedNode(child, key, nodePath, projectRoot);
155
+ }
156
+
157
+ if (absFs && fs.existsSync(absFs) && fs.statSync(absFs).isDirectory()) {
158
+ const scanned = scanFsChildren(absFs, nodePath, projectRoot);
159
+ for (const [key, child] of Object.entries(scanned)) {
160
+ if (!children[key]) {
161
+ children[key] = child;
162
+ } else {
163
+ children[key] = {
164
+ ...children[key],
165
+ fsPath: children[key].fsPath || child.fsPath,
166
+ children: { ...child.children, ...children[key].children },
167
+ };
168
+ }
169
+ }
170
+ }
171
+
172
+ return {
173
+ name,
174
+ path: nodePath,
175
+ className,
176
+ fsPath: fsRel ? posix(fsRel) : undefined,
177
+ children,
178
+ };
179
+ }
180
+
181
+ function indexByPath(node: DatamodelNode, into: Record<string, DatamodelNode>): void {
182
+ into[node.path] = node;
183
+ for (const child of Object.values(node.children)) {
184
+ indexByPath(child, into);
185
+ }
186
+ }
187
+
188
+ export function findProjectJson(projectRoot: string, explicit?: string): string | null {
189
+ const candidates = [
190
+ explicit,
191
+ path.join(projectRoot, "default.project.json"),
192
+ path.join(projectRoot, "default.project.jsonc"),
193
+ ].filter(Boolean) as string[];
194
+ for (const c of candidates) {
195
+ const abs = path.resolve(c);
196
+ if (fs.existsSync(abs)) {
197
+ return abs;
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+
203
+ export function buildDatamodelProfile(projectRoot: string, projectJsonPath?: string): DatamodelProfile | null {
204
+ const jsonPath = findProjectJson(projectRoot, projectJsonPath);
205
+ if (!jsonPath) {
206
+ return null;
207
+ }
208
+ const raw = parse(fs.readFileSync(jsonPath, "utf8")) as { name?: string; tree?: unknown };
209
+ const tree = asRecord(raw.tree) || {};
210
+ const className = typeof tree.$className === "string" ? tree.$className : "DataModel";
211
+ const children: Record<string, DatamodelNode> = {};
212
+ for (const [key, child] of Object.entries(tree)) {
213
+ if (key.startsWith("$")) {
214
+ continue;
215
+ }
216
+ children[key] = walkNamedNode(child, key, "DataModel", projectRoot);
217
+ }
218
+
219
+ const root: DatamodelNode = {
220
+ name: "DataModel",
221
+ path: "DataModel",
222
+ className,
223
+ children,
224
+ };
225
+
226
+ const byPath: Record<string, DatamodelNode> = {};
227
+ indexByPath(root, byPath);
228
+
229
+ return {
230
+ schemaVersion: DATAMODEL_SCHEMA_VERSION,
231
+ projectName: typeof raw.name === "string" ? raw.name : "game",
232
+ projectJson: posix(path.relative(projectRoot, jsonPath) || path.basename(jsonPath)),
233
+ root,
234
+ byPath,
235
+ generatedAt: new Date().toISOString(),
236
+ };
237
+ }
238
+
239
+ export function lookupChild(parent: DatamodelNode, childName: string): DatamodelNode | undefined {
240
+ return parent.children[childName];
241
+ }
242
+
243
+ export function resolvePath(profile: DatamodelProfile, segments: string[]): DatamodelNode | null {
244
+ let node: DatamodelNode = profile.root;
245
+ for (const seg of segments) {
246
+ if (seg === "game" || seg === "DataModel") {
247
+ node = profile.root;
248
+ continue;
249
+ }
250
+ const next = lookupChild(node, seg);
251
+ if (!next) {
252
+ return null;
253
+ }
254
+ node = next;
255
+ }
256
+ return node;
257
+ }
258
+
259
+ export function listChildNames(node: DatamodelNode): string[] {
260
+ return Object.keys(node.children).sort();
261
+ }
@@ -0,0 +1,67 @@
1
+ /** Shared diagnostic shape for platform analyzers. */
2
+ export interface PlatformDiagnostic {
3
+ code: string;
4
+ message: string;
5
+ line: number;
6
+ column: number;
7
+ severity: "error" | "warning" | "info";
8
+ file?: string;
9
+ }
10
+
11
+ export function lineColAt(source: string, index: number): { line: number; column: number } {
12
+ let line = 1;
13
+ let column = 1;
14
+ for (let i = 0; i < index && i < source.length; i++) {
15
+ if (source[i] === "\n") {
16
+ line++;
17
+ column = 1;
18
+ } else {
19
+ column++;
20
+ }
21
+ }
22
+ return { line, column };
23
+ }
24
+
25
+ export function stripCommentsKeepStrings(source: string): string {
26
+ let out = "";
27
+ let i = 0;
28
+ while (i < source.length) {
29
+ const c = source[i];
30
+ const next = source[i + 1];
31
+ if (c === "/" && next === "/") {
32
+ while (i < source.length && source[i] !== "\n") i++;
33
+ continue;
34
+ }
35
+ if (c === "/" && next === "*") {
36
+ i += 2;
37
+ while (i < source.length - 1 && !(source[i] === "*" && source[i + 1] === "/")) {
38
+ if (source[i] === "\n") out += "\n";
39
+ i++;
40
+ }
41
+ i += 2;
42
+ continue;
43
+ }
44
+ if (c === '"' || c === "'" || c === "`") {
45
+ const q = c;
46
+ out += c;
47
+ i++;
48
+ while (i < source.length && source[i] !== q) {
49
+ if (source[i] === "\\" && i + 1 < source.length) {
50
+ out += source[i] + source[i + 1];
51
+ i += 2;
52
+ continue;
53
+ }
54
+ out += source[i];
55
+ i++;
56
+ }
57
+ if (i < source.length) {
58
+ out += source[i];
59
+ i++;
60
+ }
61
+ continue;
62
+ }
63
+ out += c;
64
+ i++;
65
+ }
66
+ return out;
67
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * cluaupp doctor — run all platform checks and report.
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { collectSources } from "../clpp/paths.js";
8
+ import { buildRobloxTargetProfile, profileContentHash } from "../api/build-profile.js";
9
+ import { projectRoot as pkgRoot } from "../package-info.js";
10
+ import { overridesHash } from "../api/loader.js";
11
+ import { writeCapabilityProfile } from "./capability-profile.js";
12
+ import { builtinCapabilityProfile } from "./capabilities.js";
13
+ import { checkContextSafety } from "./context-check.js";
14
+ import { writeDatamodelArtifacts } from "./datamodel-generate.js";
15
+ import { checkDatamodelPaths } from "./datamodel-check.js";
16
+ import { checkParallelSafety } from "./parallel-check.js";
17
+ import { checkAuthority, loadPlatformPolicy } from "./authority-check.js";
18
+ import { checkAuthorityGraph } from "./authority-graph.js";
19
+ import { checkComponentContracts } from "./component-contracts.js";
20
+ import { checkSecurity } from "./security-check.js";
21
+ import { checkLifetime } from "./lifetime-check.js";
22
+ import { adviseOptimizer, adviceAsDiagnostics } from "./optimizer-advise.js";
23
+ import { buildProjectGraph } from "./project-graph.js";
24
+ import { checkFlareVersions } from "./flare-version.js";
25
+ import { checkGetService } from "./get-service.js";
26
+ import type { PlatformDiagnostic } from "./diagnostics.js";
27
+
28
+ export interface DoctorReport {
29
+ ok: boolean;
30
+ checks: Array<{ name: string; ok: boolean; detail?: string; diagnostics?: PlatformDiagnostic[] }>;
31
+ outOfScope: string[];
32
+ }
33
+
34
+ function verifyLockLocal(): { ok: boolean; messages: string[] } {
35
+ const lockPath = path.join(pkgRoot, "api", "roblox-api.lock.json");
36
+ if (!fs.existsSync(lockPath)) {
37
+ return { ok: false, messages: ["missing api/roblox-api.lock.json"] };
38
+ }
39
+ try {
40
+ const lock = JSON.parse(fs.readFileSync(lockPath, "utf8")) as { profileHash?: string; overridesHash?: string };
41
+ const profile = buildRobloxTargetProfile({ skipValidate: true });
42
+ const messages: string[] = [];
43
+ const ph = profileContentHash(profile);
44
+ if (lock.profileHash && lock.profileHash !== ph) {
45
+ messages.push("profile hash drift — run cluaupp api generate");
46
+ }
47
+ if (lock.overridesHash && lock.overridesHash !== overridesHash()) {
48
+ messages.push("overrides hash drift — run cluaupp api generate");
49
+ }
50
+ return { ok: messages.length === 0, messages };
51
+ } catch (err) {
52
+ return { ok: false, messages: [err instanceof Error ? err.message : String(err)] };
53
+ }
54
+ }
55
+
56
+ export function runDoctor(gameRoot: string, rootDir = "src"): DoctorReport {
57
+ const checks: DoctorReport["checks"] = [];
58
+ const srcDir = path.join(gameRoot, rootDir);
59
+ const files = fs.existsSync(srcDir) ? collectSources(srcDir) : [];
60
+ const policy = loadPlatformPolicy(gameRoot);
61
+
62
+ try {
63
+ buildRobloxTargetProfile({ skipValidate: true });
64
+ checks.push({ name: "api-registry", ok: true });
65
+ } catch (err) {
66
+ checks.push({ name: "api-registry", ok: false, detail: err instanceof Error ? err.message : String(err) });
67
+ }
68
+
69
+ const lock =
70
+ path.resolve(gameRoot) === path.resolve(pkgRoot) ? verifyLockLocal() : { ok: true, messages: ["skipped (not Cluaupp package root)"] };
71
+ checks.push({ name: "api-lock", ok: lock.ok, detail: lock.ok ? lock.messages[0] : lock.messages.join("; ") });
72
+
73
+ writeCapabilityProfile();
74
+ checks.push({ name: "context-profile", ok: true, detail: `${builtinCapabilityProfile().rules.length} rules` });
75
+
76
+ const dm = writeDatamodelArtifacts(gameRoot);
77
+ checks.push({
78
+ name: "datamodel",
79
+ ok: Boolean(dm),
80
+ detail: dm ? `${Object.keys(dm.profile.byPath).length} nodes` : "no default.project.json",
81
+ });
82
+
83
+ const allDiags: PlatformDiagnostic[] = [];
84
+ for (const file of files) {
85
+ const rel = path.relative(gameRoot, file).replace(/\\/g, "/");
86
+ const source = fs.readFileSync(file, "utf8");
87
+ allDiags.push(
88
+ ...checkContextSafety(source, rel).map((d) => ({
89
+ code: d.code,
90
+ message: d.message,
91
+ line: d.line,
92
+ column: d.column,
93
+ severity: d.severity as PlatformDiagnostic["severity"],
94
+ file: rel,
95
+ })),
96
+ );
97
+ allDiags.push(...checkParallelSafety(source, rel));
98
+ allDiags.push(...checkAuthority(source, rel, policy));
99
+ allDiags.push(
100
+ ...checkGetService(source, rel).map((d) => ({
101
+ code: "CLUAU_SVC001",
102
+ message: d.message,
103
+ line: d.line,
104
+ column: d.column,
105
+ severity: d.severity as PlatformDiagnostic["severity"],
106
+ file: rel,
107
+ })),
108
+ );
109
+ allDiags.push(...checkSecurity(source, rel));
110
+ allDiags.push(...checkLifetime(source, rel));
111
+ allDiags.push(...adviceAsDiagnostics(adviseOptimizer(source, rel), rel));
112
+ if (dm) {
113
+ allDiags.push(
114
+ ...checkDatamodelPaths(source, dm.profile).map((d) => ({
115
+ code: d.code,
116
+ message: d.message,
117
+ line: d.line,
118
+ column: d.column,
119
+ severity: d.severity as PlatformDiagnostic["severity"],
120
+ file: rel,
121
+ })),
122
+ );
123
+ }
124
+ }
125
+
126
+ if (dm && policy.components?.length) {
127
+ allDiags.push(...checkComponentContracts(dm.profile, policy.components));
128
+ }
129
+
130
+ const flare = checkFlareVersions(gameRoot, rootDir);
131
+ allDiags.push(...flare.conflicts);
132
+ allDiags.push(...checkAuthorityGraph(gameRoot, rootDir, policy));
133
+
134
+ const graph = buildProjectGraph(gameRoot, rootDir);
135
+ if (graph.cycles.length) {
136
+ allDiags.push({
137
+ code: "CLUAU_GRAPH_CYCLE",
138
+ message: `Circular includes: ${graph.cycles.map((c) => c.join(" → ")).join(" | ")}`,
139
+ line: 1,
140
+ column: 1,
141
+ severity: "error",
142
+ });
143
+ }
144
+
145
+ const errors = allDiags.filter((d) => d.severity === "error");
146
+ const warnings = allDiags.filter((d) => d.severity === "warning");
147
+ checks.push({
148
+ name: "source-analysis",
149
+ ok: errors.length === 0,
150
+ detail: `${files.length} files, ${errors.length} errors, ${warnings.length} warnings, ${allDiags.filter((d) => d.severity === "info").length} info`,
151
+ diagnostics: allDiags.slice(0, 50),
152
+ });
153
+ checks.push({
154
+ name: "project-graph",
155
+ ok: graph.cycles.length === 0,
156
+ detail: `${graph.nodes.length} nodes, ${graph.edges.length} edges, ${graph.cycles.length} cycles`,
157
+ });
158
+ checks.push({
159
+ name: "flare-versions",
160
+ ok: flare.conflicts.length === 0,
161
+ detail: `${flare.files.length} schemas`,
162
+ });
163
+
164
+ return {
165
+ ok: checks.every((c) => c.ok),
166
+ checks,
167
+ outOfScope: [
168
+ "Roblox bytecode / custom VM debugger (Studio ScriptContext + maps + CluauppNav cover source-level)",
169
+ ],
170
+ };
171
+ }
@@ -0,0 +1,61 @@
1
+ /** Flare remote versioning — detect schema version drift across .flare files. */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { collectFlareFiles, parseFlare } from "../flare/index.js";
5
+ import type { PlatformDiagnostic } from "./diagnostics.js";
6
+
7
+ export interface FlareVersionReport {
8
+ files: Array<{ file: string; name: string; version: number | null }>;
9
+ conflicts: PlatformDiagnostic[];
10
+ }
11
+
12
+ function readVersion(source: string, parsedVersion?: number): number | null {
13
+ if (typeof parsedVersion === "number" && Number.isFinite(parsedVersion)) {
14
+ return parsedVersion;
15
+ }
16
+ const m =
17
+ source.match(/^\s*version\s+(\d+)\s*;?/m) ||
18
+ source.match(/^\s*opt\s+version\s*=\s*(\d+)/im) ||
19
+ source.match(/^\s*#\s*version\s+(\d+)/m);
20
+ return m ? Number(m[1]) : null;
21
+ }
22
+
23
+ export function checkFlareVersions(projectRoot: string, rootDir = "src"): FlareVersionReport {
24
+ const srcDir = path.join(projectRoot, rootDir);
25
+ const files = fs.existsSync(srcDir) ? collectFlareFiles(srcDir) : [];
26
+ const byName = new Map<string, Array<{ file: string; version: number | null }>>();
27
+ const listed: FlareVersionReport["files"] = [];
28
+
29
+ for (const file of files) {
30
+ const source = fs.readFileSync(file, "utf8");
31
+ const rel = path.relative(projectRoot, file).replace(/\\/g, "/");
32
+ let name = path.basename(file, ".flare");
33
+ let parsedVersion: number | undefined;
34
+ try {
35
+ const parsed = parseFlare(source, rel, name);
36
+ name = parsed.name || name;
37
+ parsedVersion = parsed.version;
38
+ } catch {
39
+ /* parse may fail on partial */
40
+ }
41
+ const version = readVersion(source, parsedVersion);
42
+ listed.push({ file: rel, name, version });
43
+ if (!byName.has(name)) byName.set(name, []);
44
+ byName.get(name)!.push({ file: rel, version });
45
+ }
46
+
47
+ const conflicts: PlatformDiagnostic[] = [];
48
+ for (const [name, entries] of byName) {
49
+ const versions = new Set(entries.map((e) => e.version).filter((v) => v !== null));
50
+ if (versions.size > 1) {
51
+ conflicts.push({
52
+ code: "CLUAU_NET_VERSION",
53
+ message: `Flare contract "${name}" has conflicting versions: ${[...versions].join(", ")} across ${entries.map((e) => e.file).join(", ")}`,
54
+ line: 1,
55
+ column: 1,
56
+ severity: "error",
57
+ });
58
+ }
59
+ }
60
+ return { files: listed, conflicts };
61
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * GetService\<T\>() — Cluaupp host feature (Roblox Target), not a CL++ language primitive.
3
+ *
4
+ * CL++ parses `GetService<Players>()` as a normal generic call and may emit
5
+ * `game:GetService("Players")`. Cluaupp owns:
6
+ * - which names are services (Canonical Registry)
7
+ * - IntelliSense stubs (`#include <clpp/roblox.clh>`)
8
+ * - Context Safety / capability rules for services
9
+ * - unknown-service diagnostics at build time
10
+ */
11
+
12
+ import type { CompileDiagnostic } from "../clpp/contract.js";
13
+ import type { RobloxTargetProfile } from "../api/model.js";
14
+ import { buildRobloxTargetProfile } from "../api/build-profile.js";
15
+ import { lineColAt, stripCommentsKeepStrings } from "./diagnostics.js";
16
+ import { resolveGetServiceType } from "./roblox.js";
17
+
18
+ const GET_SERVICE_CALL =
19
+ /\bGetService\s*(?:<\s*([A-Za-z_]\w*)\s*>\s*\(\s*\)|\(\s*["']([A-Za-z_]\w*)["']\s*\))/g;
20
+
21
+ export interface GetServiceUse {
22
+ service: string;
23
+ index: number;
24
+ generic: boolean;
25
+ }
26
+
27
+ /** Extract GetService\<T\>() / GetService("T") uses from CL++ source. */
28
+ export function collectGetServiceUses(source: string): GetServiceUse[] {
29
+ const clean = stripCommentsKeepStrings(source);
30
+ const out: GetServiceUse[] = [];
31
+ GET_SERVICE_CALL.lastIndex = 0;
32
+ let m: RegExpExecArray | null;
33
+ while ((m = GET_SERVICE_CALL.exec(clean))) {
34
+ const service = m[1] || m[2];
35
+ if (!service) continue;
36
+ out.push({ service, index: m.index, generic: Boolean(m[1]) });
37
+ }
38
+ return out;
39
+ }
40
+
41
+ export function checkGetService(
42
+ source: string,
43
+ fileName: string,
44
+ profile?: RobloxTargetProfile,
45
+ ): CompileDiagnostic[] {
46
+ const p = profile || buildRobloxTargetProfile({ skipValidate: true });
47
+ const clean = stripCommentsKeepStrings(source);
48
+ const out: CompileDiagnostic[] = [];
49
+ for (const use of collectGetServiceUses(source)) {
50
+ if (resolveGetServiceType(use.service, p)) {
51
+ continue;
52
+ }
53
+ const { line, column } = lineColAt(clean, use.index);
54
+ out.push({
55
+ message: `CLUAU_SVC001: unknown service \`${use.service}\` for GetService (Cluaupp registry)`,
56
+ line,
57
+ column,
58
+ severity: "error",
59
+ });
60
+ }
61
+ return out;
62
+ }
63
+
64
+ export function assertGetServiceSafe(
65
+ source: string,
66
+ fileName: string,
67
+ profile?: RobloxTargetProfile,
68
+ ): void {
69
+ const diags = checkGetService(source, fileName, profile);
70
+ if (diags.length === 0) {
71
+ return;
72
+ }
73
+ const body = diags.map((d) => ` ${fileName}:${d.line}:${d.column}: ${d.message}`).join("\n");
74
+ throw new Error(`cluaupp: GetService check failed\n${body}`);
75
+ }
76
+
77
+ /**
78
+ * Normalize free-standing GetService leftovers in Luau to game:GetService("Name").
79
+ * Prefer CL++ emit; this is a host safety net for host-owned semantics.
80
+ */
81
+ export function rewriteGetServiceLuau(luau: string): string {
82
+ return String(luau)
83
+ .replace(/\bGetService\s*<\s*([A-Za-z_]\w*)\s*>\s*\(\s*\)/g, 'game:GetService("$1")')
84
+ .replace(/\bGetService\s*\(\s*["']([A-Za-z_]\w*)["']\s*\)/g, 'game:GetService("$1")');
85
+ }
@@ -0,0 +1,45 @@
1
+ /** Lifetime / leak heuristics (CLUAU-LIFE*). */
2
+ import { type PlatformDiagnostic, lineColAt, stripCommentsKeepStrings } from "./diagnostics.js";
3
+
4
+ export function checkLifetime(source: string, fileName: string): PlatformDiagnostic[] {
5
+ const clean = stripCommentsKeepStrings(source);
6
+ const out: PlatformDiagnostic[] = [];
7
+ const connectRe = /\.Connect\s*\(/g;
8
+ let m: RegExpExecArray | null;
9
+ while ((m = connectRe.exec(clean))) {
10
+ const before = clean.slice(Math.max(0, m.index - 8), m.index);
11
+ if (before.includes("~>") || before.endsWith("::")) {
12
+ continue;
13
+ }
14
+ const window = clean.slice(Math.max(0, m.index - 120), m.index + 80);
15
+ if (window.includes("janitor") || window.includes("Sweep") || /\.Add\s*\(/.test(window)) {
16
+ continue;
17
+ }
18
+ const { line, column } = lineColAt(clean, m.index);
19
+ out.push({
20
+ code: "CLUAU_LIFE001",
21
+ message: "Connection created without Sweep/`~>` / janitor.Add nearby — possible leak",
22
+ line,
23
+ column,
24
+ severity: "warning",
25
+ file: fileName,
26
+ });
27
+ }
28
+ const spawnRe = /\bspawn\s*\{/g;
29
+ while ((m = spawnRe.exec(clean))) {
30
+ const bodyEnd = clean.indexOf("}", m.index);
31
+ const body = clean.slice(m.index, bodyEnd > 0 ? bodyEnd : m.index + 200);
32
+ if (!/cancel|Cleanup|Destroy|return/.test(body) && /while\s*\(|for\s*\(/.test(body)) {
33
+ const { line, column } = lineColAt(clean, m.index);
34
+ out.push({
35
+ code: "CLUAU_LIFE002",
36
+ message: "spawn { } with a loop and no obvious cancellation path",
37
+ line,
38
+ column,
39
+ severity: "info",
40
+ file: fileName,
41
+ });
42
+ }
43
+ }
44
+ return out;
45
+ }