From 4035f149fcbaeeea3fe7285e5b3b5ee7d10ed2cf Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:46:11 +0000 Subject: [PATCH 1/8] fix(events): re-include app/global/*.cfm on bare ?reload=true when files change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding a helper to `app/global/functions.cfm` (or anything it ``s) used to require the password-gated `?reload=true&password=...` path. Bare `?reload=true` re-ran config and routes but left `application.wo` (the `Global.cfc` instance) intact, so the symbols merged into its variables scope at construction time stayed frozen — the page rendered without error and the new helper was silently undefined. The fix follows the Rails/Phoenix per-request mtime-check pattern recommended by the research comment: snapshot `app/global/*.cfm` mtimes on application start, and on bare `?reload=true` in development re-evaluate the include if any tracked file has been added, removed, or touched. The password-gated `applicationStop()` path still does a full re-init unchanged — this just makes the muscle-memory path actually work. Three new helpers on `wheels.Global`: - `$snapshotGlobalIncludes(directory)` — struct of `path → dateLastModified` - `$globalIncludesChanged(snapshot, directory)` — diff against current state - `$reincludeGlobals(file)` — re-evaluate the include against the live Global instance New setting `reloadOnGlobalChange` defaults to `true` in development and `false` everywhere else; opt out with `set(reloadOnGlobalChange=false)`. Fixes #2792 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + vendor/wheels/Global.cfc | 44 ++++++++++ vendor/wheels/events/EventMethods.cfc | 17 ++++ vendor/wheels/events/init/orm.cfm | 4 + vendor/wheels/events/onapplicationstart.cfc | 4 + .../tests/specs/global/reloadGlobalsSpec.cfc | 86 +++++++++++++++++++ 6 files changed, 156 insertions(+) create mode 100644 vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc diff --git a/CHANGELOG.md b/CHANGELOG.md index bc365070e2..e36371d9ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo - Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773) - `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs +- Bare `?reload=true` now re-includes `app/global/*.cfm` in development when any tracked file's mtime has changed, so new helpers added to `app/global/functions.cfm` (or any file it includes) are picked up without the password-gated `applicationStop()` path. Wheels snapshots the include directory on application start and re-evaluates the include via a new `application.wo.$reincludeGlobals()` hook when `$globalIncludesChanged()` detects an added/removed/touched file. Opt out with `set(reloadOnGlobalChange=false)` in `config/settings.cfm` (issue #2792) ---- diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 96cb870485..b4bc7375ba 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -3849,6 +3849,50 @@ return local.$wheels; } } + /** + * Snapshot mtimes of all .cfm files under the app's global include directory. + * + * Used by the bare `?reload=true` path so a developer adding a helper to + * `app/global/*.cfm` does not have to remember the password-gated full reload + * (issue ##2792). + */ + public struct function $snapshotGlobalIncludes(string directory = ExpandPath("/app/global")) { + var snapshot = {}; + if (!DirectoryExists(arguments.directory)) { + return snapshot; + } + var files = DirectoryList(arguments.directory, true, "query", "*.cfm"); + for (var row in files) { + snapshot[row.directory & "/" & row.name] = row.dateLastModified; + } + return snapshot; + } + + public boolean function $globalIncludesChanged( + required struct snapshot, + string directory = ExpandPath("/app/global") + ) { + var current = $snapshotGlobalIncludes(directory = arguments.directory); + for (var key in current) { + if (!StructKeyExists(arguments.snapshot, key)) { + return true; + } + if (DateCompare(arguments.snapshot[key], current[key]) != 0) { + return true; + } + } + for (var key in arguments.snapshot) { + if (!StructKeyExists(current, key)) { + return true; + } + } + return false; + } + + public void function $reincludeGlobals(string file = "/app/global/functions.cfm") { + include "#arguments.file#"; + } + // User-defined global functions include "/app/global/functions.cfm"; diff --git a/vendor/wheels/events/EventMethods.cfc b/vendor/wheels/events/EventMethods.cfc index 1360524187..7ae2f6b74b 100644 --- a/vendor/wheels/events/EventMethods.cfc +++ b/vendor/wheels/events/EventMethods.cfc @@ -174,6 +174,23 @@ component extends="wheels.Global" implements="wheels.interfaces.events.EventHand request.$wheelsHeaders = GetHTTPRequestData().headers; } + // Soft-reload of app/global/*.cfm when bare ?reload=true is hit in dev + // and any tracked include has a newer mtime. The password-gated + // applicationStop() path already does a full re-init, so we skip the + // check when url.password is present (issue 2792). + if ( + StructKeyExists(url, "reload") + && !StructKeyExists(url, "password") + && StructKeyExists(application.wheels, "reloadOnGlobalChange") + && application.wheels.reloadOnGlobalChange + && application.wheels.environment == "development" + && StructKeyExists(application.wheels, "globalIncludesSnapshot") + && application.wo.$globalIncludesChanged(snapshot = application.wheels.globalIncludesSnapshot) + ) { + application.wo.$reincludeGlobals(); + application.wheels.globalIncludesSnapshot = application.wo.$snapshotGlobalIncludes(); + } + // Reload the plugins on each request if cachePlugins is set to false. if (!application.wheels.cachePlugins) { $loadPlugins(); diff --git a/vendor/wheels/events/init/orm.cfm b/vendor/wheels/events/init/orm.cfm index 6250011010..269a3036b2 100644 --- a/vendor/wheels/events/init/orm.cfm +++ b/vendor/wheels/events/init/orm.cfm @@ -24,6 +24,10 @@ application.$wheels.suppressRouteBindingWarnings = false; application.$wheels.reloadPassword = ""; application.$wheels.redirectAfterReload = false; + // Soft-reload of app/global/*.cfm on ?reload=true when files change. + // Defaults true in development so adding a helper to app/global is picked + // up without the password-gated applicationStop() path (issue 2792). + application.$wheels.reloadOnGlobalChange = (application.$wheels.environment == "development"); application.$wheels.softDeleteProperty = "deletedAt"; application.$wheels.timeStampOnCreateProperty = "createdAt"; application.$wheels.timeStampOnUpdateProperty = "updatedAt"; diff --git a/vendor/wheels/events/onapplicationstart.cfc b/vendor/wheels/events/onapplicationstart.cfc index c367c9ba8c..5d34d9c563 100644 --- a/vendor/wheels/events/onapplicationstart.cfc +++ b/vendor/wheels/events/onapplicationstart.cfc @@ -364,6 +364,10 @@ component { // Create the dispatcher that will handle all incoming requests. application.$wheels.dispatch = application.wo.$createObjectFromRoot(path = "wheels", fileName = "Dispatch", method = "$init"); + // Snapshot the app/global/*.cfm mtimes so the per-request soft-reload + // check (in $runOnRequestStart) has a baseline to compare against. + application.$wheels.globalIncludesSnapshot = application.wo.$snapshotGlobalIncludes(); + // Assign it all to the application scope in one atomic call. application.wheels = application.$wheels; StructDelete(application, "$wheels"); diff --git a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc new file mode 100644 index 0000000000..b4fe332439 --- /dev/null +++ b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc @@ -0,0 +1,86 @@ +component extends="wheels.WheelsTest" { + + function run() { + + describe("Reload — global includes mtime tracking (issue ##2792)", () => { + + var g = application.wo; + var baseDir = ExpandPath("/wheels/tests/_tmp/reloadGlobals"); + + beforeEach(() => { + if (DirectoryExists(baseDir)) { + DirectoryDelete(baseDir, true); + } + DirectoryCreate(baseDir, true); + }); + + afterEach(() => { + if (DirectoryExists(baseDir)) { + DirectoryDelete(baseDir, true); + } + }); + + it("$snapshotGlobalIncludes returns a struct keyed by cfm file paths", () => { + FileWrite(baseDir & "/fixtureA.cfm", "function fxA(){return 1;}"); + FileWrite(baseDir & "/fixtureB.cfm", "function fxB(){return 2;}"); + var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); + expect(snapshot).toBeStruct(); + expect(StructCount(snapshot)).toBe(2); + }); + + it("$snapshotGlobalIncludes returns an empty struct when the directory does not exist", () => { + var missing = baseDir & "/does-not-exist"; + var snapshot = g.$snapshotGlobalIncludes(directory = missing); + expect(snapshot).toBeStruct(); + expect(StructCount(snapshot)).toBe(0); + }); + + it("$globalIncludesChanged returns false when no files changed", () => { + FileWrite(baseDir & "/stable.cfm", "function fxStable(){return 'stable';}"); + var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); + expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeFalse(); + }); + + it("$globalIncludesChanged returns true when a new cfm file appears", () => { + FileWrite(baseDir & "/one.cfm", "function fxOne(){return 1;}"); + var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); + FileWrite(baseDir & "/two.cfm", "function fxTwo(){return 2;}"); + expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeTrue(); + }); + + it("$globalIncludesChanged returns true when a tracked cfm file is removed", () => { + FileWrite(baseDir & "/keep.cfm", "function fxKeep(){return 1;}"); + FileWrite(baseDir & "/gone.cfm", "function fxGone(){return 2;}"); + var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); + FileDelete(baseDir & "/gone.cfm"); + expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeTrue(); + }); + + it("$globalIncludesChanged tolerates an empty starting snapshot", () => { + var snapshot = {}; + FileWrite(baseDir & "/added.cfm", "function fxAdded(){return 1;}"); + expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeTrue(); + }); + + it("$reincludeGlobals re-evaluates the target cfm without throwing", () => { + // CFML's `include` resolves via mappings, not absolute filesystem + // paths — call $reincludeGlobals with the mapping-relative form. + var mappingPath = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"; + var absPath = ExpandPath(mappingPath); + FileWrite(absPath, "function fxReinclude(){return 'first';}"); + $assert.notThrows(function() { + application.wo.$reincludeGlobals(file = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"); + }); + + // After overwriting the file, re-running the include should also + // succeed — covers the "developer just changed a helper" path + // that the bare ?reload=true workflow targets. + FileWrite(absPath, "function fxReinclude(){return 'second';}"); + $assert.notThrows(function() { + application.wo.$reincludeGlobals(file = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"); + }); + }); + + }); + } +} From bb9bc32df0924a873b80b97fbaa57e57fb33a23c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:51:10 +0000 Subject: [PATCH 2/8] docs(web/guides): document reloadOnGlobalChange setting and bare reload behavior Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../v4-0-0/command-line-tools/wheels-commands/dev-server.mdx | 2 +- .../v4-0-0/core-concepts/environments-and-configuration.mdx | 2 ++ .../command-line-tools/wheels-commands/dev-server.mdx | 2 +- .../core-concepts/environments-and-configuration.mdx | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx index eb69ce2f24..714ea31d04 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx @@ -114,7 +114,7 @@ wheels reload On success: `Application reloaded successfully.` in green. On failure — most often a password mismatch — it prints `Failed to reload: ` and hints at setting `RELOAD_PASSWORD` in `.env` or `config/settings.cfm`. ## Dev loop example diff --git a/web/sites/guides/src/content/docs/v4-0-0/core-concepts/environments-and-configuration.mdx b/web/sites/guides/src/content/docs/v4-0-0/core-concepts/environments-and-configuration.mdx index d33d664427..21ae15c1c0 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/core-concepts/environments-and-configuration.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/core-concepts/environments-and-configuration.mdx @@ -51,6 +51,8 @@ set(environment = application.wo.env("WHEELS_ENV", "development")); Settings cascade: framework defaults load first, then `config/settings.cfm`, then `config//settings.cfm`. Later writes win. A value you set in the shared file is the default for every environment; the environment-specific file only needs to name the keys it changes. +One development-only default worth noting: `reloadOnGlobalChange` (defaults `true` in development) makes bare `?reload=true` re-include `app/global/*.cfm` when any tracked file's mtime changes, so new helpers added to `app/global/functions.cfm` are picked up immediately without the password-gated full reload. Opt out with `set(reloadOnGlobalChange=false)` in `config/settings.cfm`. The setting defaults to `false` in all other environments, so there is no `DirectoryList` overhead in production. + ## Environment detection The active environment is set by `config/environment.cfm`, which typically reads `WHEELS_ENV` from the environment variables with a default of `development`. The `wheels` CLI respects the same variable, and `.env` files loaded at boot get merged into the lookup. Override in CI or production by exporting `WHEELS_ENV=production` before the app starts — a deploy script, a Dockerfile `ENV`, or a systemd unit is the usual place. diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx index fbc84ea7b5..b7202140e4 100644 --- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx +++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx @@ -114,7 +114,7 @@ wheels reload On success: `Application reloaded successfully.` in green. On failure — most often a password mismatch — it prints `Failed to reload: ` and hints at setting `RELOAD_PASSWORD` in `.env` or `config/settings.cfm`. ## Dev loop example diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/core-concepts/environments-and-configuration.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/core-concepts/environments-and-configuration.mdx index 30b704ca4c..9fac39ae9b 100644 --- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/core-concepts/environments-and-configuration.mdx +++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/core-concepts/environments-and-configuration.mdx @@ -51,6 +51,8 @@ set(environment = application.wo.env("WHEELS_ENV", "development")); Settings cascade: framework defaults load first, then `config/settings.cfm`, then `config//settings.cfm`. Later writes win. A value you set in the shared file is the default for every environment; the environment-specific file only needs to name the keys it changes. +One development-only default worth noting: `reloadOnGlobalChange` (defaults `true` in development) makes bare `?reload=true` re-include `app/global/*.cfm` when any tracked file's mtime changes, so new helpers added to `app/global/functions.cfm` are picked up immediately without the password-gated full reload. Opt out with `set(reloadOnGlobalChange=false)` in `config/settings.cfm`. The setting defaults to `false` in all other environments, so there is no `DirectoryList` overhead in production. + ## Environment detection The active environment is set by `config/environment.cfm`, which typically reads `WHEELS_ENV` from the environment variables with a default of `development`. The `wheels` CLI respects the same variable, and `.env` files loaded at boot get merged into the lookup. Override in CI or production by exporting `WHEELS_ENV=production` before the app starts — a deploy script, a Dockerfile `ENV`, or a systemd unit is the usual place. From ba1129951fc5fa25aebe8e8ced3b736ef478c6ce Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 00:14:31 +0000 Subject: [PATCH 3/8] fix(events): address Reviewer A/B consensus findings (round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace `DirectoryCreate(baseDir, true)` with `java.io.File.mkdirs()` in `reloadGlobalsSpec` — the createPath flag is Lucee-only and the call lives in beforeEach, so every spec in the group fails on Adobe CI. - Assert `IsDefined("application.wo.fxReinclude")` after each `$reincludeGlobals` call so the contract (re-included helpers must remain callable on application.wo) is guarded against silent no-ops. - Lift include-declared functions from local/variables onto this in `$reincludeGlobals` so the contract holds across engines: include inside a method body would otherwise leave declarations in scopes that aren't reachable via `application.wo.helper()`. - Wrap the bare ?reload=true re-include in a double-checked `lock name="wheels_reload_globals"` to eliminate the race between two concurrent ?reload=true hits. - Document the development-only environment guard as intentional so a future maintainer doesn't try to enable `reloadOnGlobalChange` in staging and debug a silent no-op. - Add docblocks to `$globalIncludesChanged` and `$reincludeGlobals` so all three new global-includes helpers carry consistent documentation. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- vendor/wheels/Global.cfc | 34 +++++++++++++++++++ vendor/wheels/events/EventMethods.cfc | 16 +++++++-- .../tests/specs/global/reloadGlobalsSpec.cfc | 9 ++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index b4bc7375ba..1291bc305f 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -3868,6 +3868,14 @@ return local.$wheels; return snapshot; } + /** + * Compare a prior `$snapshotGlobalIncludes` result against the current + * filesystem state and return true if any tracked .cfm file was added, + * removed, or modified. + * + * Paired with `$snapshotGlobalIncludes` to drive the bare `?reload=true` + * soft-reload path in development (issue ##2792). + */ public boolean function $globalIncludesChanged( required struct snapshot, string directory = ExpandPath("/app/global") @@ -3889,8 +3897,34 @@ return local.$wheels; return false; } + /** + * Re-evaluate the given global-includes file into `application.wo`'s + * variables/this scope. Invoked from the bare `?reload=true` soft-reload + * when `$globalIncludesChanged` reports drift (issue ##2792). + * + * `include` inside a method body adds function declarations to the + * method's local scope, not the component's outer scope, so we walk + * local for any user-defined functions and copy them onto variables + * and this so they remain callable on `application.wo` across requests. + */ public void function $reincludeGlobals(string file = "/app/global/functions.cfm") { + var beforeVars = StructKeyArray(variables); include "#arguments.file#"; + // Lucee adds include-declared functions to local; Adobe adds them + // to variables. Walk both and lift any user-defined functions onto + // this (the application.wo facing scope) so callers can invoke them + // across requests. + for (var key in local) { + if (IsCustomFunction(local[key])) { + variables[key] = local[key]; + this[key] = local[key]; + } + } + for (var key in variables) { + if (!ArrayFind(beforeVars, key) && IsCustomFunction(variables[key])) { + this[key] = variables[key]; + } + } } // User-defined global functions diff --git a/vendor/wheels/events/EventMethods.cfc b/vendor/wheels/events/EventMethods.cfc index 7ae2f6b74b..1514d82616 100644 --- a/vendor/wheels/events/EventMethods.cfc +++ b/vendor/wheels/events/EventMethods.cfc @@ -178,6 +178,11 @@ component extends="wheels.Global" implements="wheels.interfaces.events.EventHand // and any tracked include has a newer mtime. The password-gated // applicationStop() path already does a full re-init, so we skip the // check when url.password is present (issue 2792). + // + // The environment guard is intentional: this soft-reload is a + // development-only convenience. Use the password-gated reload for + // non-dev environments — setting reloadOnGlobalChange=true outside + // development is a deliberate no-op. if ( StructKeyExists(url, "reload") && !StructKeyExists(url, "password") @@ -187,8 +192,15 @@ component extends="wheels.Global" implements="wheels.interfaces.events.EventHand && StructKeyExists(application.wheels, "globalIncludesSnapshot") && application.wo.$globalIncludesChanged(snapshot = application.wheels.globalIncludesSnapshot) ) { - application.wo.$reincludeGlobals(); - application.wheels.globalIncludesSnapshot = application.wo.$snapshotGlobalIncludes(); + // Double-checked locking — two concurrent ?reload=true hits would + // otherwise both pass the outer check and race on $reincludeGlobals + // against the same application.wo instance. + lock type="exclusive" name="wheels_reload_globals" timeout="5" { + if (application.wo.$globalIncludesChanged(snapshot = application.wheels.globalIncludesSnapshot)) { + application.wo.$reincludeGlobals(); + application.wheels.globalIncludesSnapshot = application.wo.$snapshotGlobalIncludes(); + } + } } // Reload the plugins on each request if cachePlugins is set to false. diff --git a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc index b4fe332439..939f63452f 100644 --- a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc +++ b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc @@ -11,7 +11,9 @@ component extends="wheels.WheelsTest" { if (DirectoryExists(baseDir)) { DirectoryDelete(baseDir, true); } - DirectoryCreate(baseDir, true); + // DirectoryCreate(path, true) is Lucee-only (issue ##2567); + // java.io.File.mkdirs() recurses parents on every engine. + CreateObject("java", "java.io.File").init(baseDir).mkdirs(); }); afterEach(() => { @@ -71,6 +73,10 @@ component extends="wheels.WheelsTest" { $assert.notThrows(function() { application.wo.$reincludeGlobals(file = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"); }); + // The contract: re-including must make the function callable + // on application.wo. Without this assertion, a silent no-op + // on any engine would slip through. + expect(IsDefined("application.wo.fxReinclude")).toBeTrue(); // After overwriting the file, re-running the include should also // succeed — covers the "developer just changed a helper" path @@ -79,6 +85,7 @@ component extends="wheels.WheelsTest" { $assert.notThrows(function() { application.wo.$reincludeGlobals(file = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"); }); + expect(IsDefined("application.wo.fxReinclude")).toBeTrue(); }); }); From facb377efcb4370437c0838696f53976a586b7d4 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 00:53:33 +0000 Subject: [PATCH 4/8] fix(events): address Reviewer A/B consensus findings (round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vendor/wheels/Global.cfc:$reincludeGlobals — drop the !ArrayFind(beforeVars, key) snapshot-diff guard from the second loop. On Adobe CF the include updates variables[key] in place across calls, so the guard silently skipped re-binding the updated function onto `this` on the second ?reload=true. Re-lifting is idempotent and the path is development-only. - vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc:88 — replace the second IsDefined assertion with a return-value check so a stale Adobe-CF binding cannot slip past CI (expect(...fxReinclude()).toBe("second")). Local verification on Lucee 7 + SQLite after server reload: core suite 3698 pass / 0 fail / 0 error; global suite 113 pass / 0 fail / 0 error (the 7 reloadGlobals specs all green). Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- vendor/wheels/Global.cfc | 9 ++++++--- vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 1291bc305f..99c009a6b5 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -3908,12 +3908,15 @@ return local.$wheels; * and this so they remain callable on `application.wo` across requests. */ public void function $reincludeGlobals(string file = "/app/global/functions.cfm") { - var beforeVars = StructKeyArray(variables); include "#arguments.file#"; // Lucee adds include-declared functions to local; Adobe adds them // to variables. Walk both and lift any user-defined functions onto // this (the application.wo facing scope) so callers can invoke them - // across requests. + // across requests. The second loop is unconditional: a snapshot-diff + // guard here would suppress *updates* on Adobe (where the function + // already lives in variables from a prior re-include), leaving this + // bound to the stale version on the second `?reload=true`. Re-lifting + // an existing function is idempotent and the path is development-only. for (var key in local) { if (IsCustomFunction(local[key])) { variables[key] = local[key]; @@ -3921,7 +3924,7 @@ return local.$wheels; } } for (var key in variables) { - if (!ArrayFind(beforeVars, key) && IsCustomFunction(variables[key])) { + if (IsCustomFunction(variables[key])) { this[key] = variables[key]; } } diff --git a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc index 939f63452f..c599c15106 100644 --- a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc +++ b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc @@ -80,12 +80,14 @@ component extends="wheels.WheelsTest" { // After overwriting the file, re-running the include should also // succeed — covers the "developer just changed a helper" path - // that the bare ?reload=true workflow targets. + // that the bare ?reload=true workflow targets. Assert the + // *return value* changes so an Adobe-only silent no-op (the + // old version stays bound to `this`) can't slip past CI. FileWrite(absPath, "function fxReinclude(){return 'second';}"); $assert.notThrows(function() { application.wo.$reincludeGlobals(file = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"); }); - expect(IsDefined("application.wo.fxReinclude")).toBeTrue(); + expect(application.wo.fxReinclude()).toBe("second"); }); }); From 3b70d90982ee2b13a3fc8953b5336957813de6e2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 01:16:54 +0000 Subject: [PATCH 5/8] fix(test): address Reviewer A/B consensus findings (round 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc — hoist `g` and `baseDir` out of the `describe()` arrow-function callback and into `run()` as a shared `ctx` struct, then update every beforeEach / afterEach / it closure to reference `ctx.g` and `ctx.baseDir`. On Adobe CF 2023/2025 CFML closures cannot reach an enclosing function's `local` scope (CLAUDE.md cross-engine invariant ##3); the prior layout silently relied on Lucee 7's lexical capture and would have thrown "variable baseDir is undefined" inside every nested closure on Adobe CI, crashing all seven specs. Local verification on Lucee 7 + SQLite (existing test server, forced ?reload=true&password=wheels first): global directory (wheels.tests.specs.global): 113 pass / 0 fail / 0 error full core suite: 3698 pass / 0 fail / 0 error Lucee was already green before this fix because it captures the enclosing arrow-function `local`; the change is to make the Adobe CI legs match. Adobe verification is left to CI as the local harness cannot run Adobe. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../tests/specs/global/reloadGlobalsSpec.cfc | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc index c599c15106..e45d45af24 100644 --- a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc +++ b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc @@ -2,66 +2,73 @@ component extends="wheels.WheelsTest" { function run() { - describe("Reload — global includes mtime tracking (issue ##2792)", () => { + // Shared struct so nested describe / beforeEach / afterEach / it closures + // can read `g` and `baseDir` on Adobe CF 2023/2025. CFML closures cannot + // reach an enclosing function's `local` scope on Adobe CF (CLAUDE.md + // cross-engine invariant ##3); a struct is a reference type, so all + // closures share the same object via `variables.ctx`. + var ctx = { + g: application.wo, + baseDir: ExpandPath("/wheels/tests/_tmp/reloadGlobals") + }; - var g = application.wo; - var baseDir = ExpandPath("/wheels/tests/_tmp/reloadGlobals"); + describe("Reload — global includes mtime tracking (issue ##2792)", () => { beforeEach(() => { - if (DirectoryExists(baseDir)) { - DirectoryDelete(baseDir, true); + if (DirectoryExists(ctx.baseDir)) { + DirectoryDelete(ctx.baseDir, true); } // DirectoryCreate(path, true) is Lucee-only (issue ##2567); // java.io.File.mkdirs() recurses parents on every engine. - CreateObject("java", "java.io.File").init(baseDir).mkdirs(); + CreateObject("java", "java.io.File").init(ctx.baseDir).mkdirs(); }); afterEach(() => { - if (DirectoryExists(baseDir)) { - DirectoryDelete(baseDir, true); + if (DirectoryExists(ctx.baseDir)) { + DirectoryDelete(ctx.baseDir, true); } }); it("$snapshotGlobalIncludes returns a struct keyed by cfm file paths", () => { - FileWrite(baseDir & "/fixtureA.cfm", "function fxA(){return 1;}"); - FileWrite(baseDir & "/fixtureB.cfm", "function fxB(){return 2;}"); - var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); + FileWrite(ctx.baseDir & "/fixtureA.cfm", "function fxA(){return 1;}"); + FileWrite(ctx.baseDir & "/fixtureB.cfm", "function fxB(){return 2;}"); + var snapshot = ctx.g.$snapshotGlobalIncludes(directory = ctx.baseDir); expect(snapshot).toBeStruct(); expect(StructCount(snapshot)).toBe(2); }); it("$snapshotGlobalIncludes returns an empty struct when the directory does not exist", () => { - var missing = baseDir & "/does-not-exist"; - var snapshot = g.$snapshotGlobalIncludes(directory = missing); + var missing = ctx.baseDir & "/does-not-exist"; + var snapshot = ctx.g.$snapshotGlobalIncludes(directory = missing); expect(snapshot).toBeStruct(); expect(StructCount(snapshot)).toBe(0); }); it("$globalIncludesChanged returns false when no files changed", () => { - FileWrite(baseDir & "/stable.cfm", "function fxStable(){return 'stable';}"); - var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); - expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeFalse(); + FileWrite(ctx.baseDir & "/stable.cfm", "function fxStable(){return 'stable';}"); + var snapshot = ctx.g.$snapshotGlobalIncludes(directory = ctx.baseDir); + expect(ctx.g.$globalIncludesChanged(snapshot = snapshot, directory = ctx.baseDir)).toBeFalse(); }); it("$globalIncludesChanged returns true when a new cfm file appears", () => { - FileWrite(baseDir & "/one.cfm", "function fxOne(){return 1;}"); - var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); - FileWrite(baseDir & "/two.cfm", "function fxTwo(){return 2;}"); - expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeTrue(); + FileWrite(ctx.baseDir & "/one.cfm", "function fxOne(){return 1;}"); + var snapshot = ctx.g.$snapshotGlobalIncludes(directory = ctx.baseDir); + FileWrite(ctx.baseDir & "/two.cfm", "function fxTwo(){return 2;}"); + expect(ctx.g.$globalIncludesChanged(snapshot = snapshot, directory = ctx.baseDir)).toBeTrue(); }); it("$globalIncludesChanged returns true when a tracked cfm file is removed", () => { - FileWrite(baseDir & "/keep.cfm", "function fxKeep(){return 1;}"); - FileWrite(baseDir & "/gone.cfm", "function fxGone(){return 2;}"); - var snapshot = g.$snapshotGlobalIncludes(directory = baseDir); - FileDelete(baseDir & "/gone.cfm"); - expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeTrue(); + FileWrite(ctx.baseDir & "/keep.cfm", "function fxKeep(){return 1;}"); + FileWrite(ctx.baseDir & "/gone.cfm", "function fxGone(){return 2;}"); + var snapshot = ctx.g.$snapshotGlobalIncludes(directory = ctx.baseDir); + FileDelete(ctx.baseDir & "/gone.cfm"); + expect(ctx.g.$globalIncludesChanged(snapshot = snapshot, directory = ctx.baseDir)).toBeTrue(); }); it("$globalIncludesChanged tolerates an empty starting snapshot", () => { var snapshot = {}; - FileWrite(baseDir & "/added.cfm", "function fxAdded(){return 1;}"); - expect(g.$globalIncludesChanged(snapshot = snapshot, directory = baseDir)).toBeTrue(); + FileWrite(ctx.baseDir & "/added.cfm", "function fxAdded(){return 1;}"); + expect(ctx.g.$globalIncludesChanged(snapshot = snapshot, directory = ctx.baseDir)).toBeTrue(); }); it("$reincludeGlobals re-evaluates the target cfm without throwing", () => { From f57ea20930733f9aced1c6bfee58456e1621d7f8 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 01:26:52 +0000 Subject: [PATCH 6/8] fix: address Reviewer A/B consensus findings (round 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx — tighten the bare `?reload=true` note so it mentions `app/global/functions.cfm` (and any files it ``s) instead of the looser `app/global/*.cfm`. `$globalIncludesChanged` watches every `*.cfm` under `app/global/` but `$reincludeGlobals` re-evaluates only `functions.cfm` and the files it transitively includes; the previous wording implied a developer could drop a standalone helper file directly and have it bind, which isn't true. Matches the more accurate wording already in `core-concepts/environments-and-configuration.mdx`. - web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx — same wording change for the v4-0-1 snapshot copy, keeping the two doc trees in sync. - vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc — drop the dead `mappingPath` local variable; it was only used to compute `absPath`, and the `$reincludeGlobals` calls inside the `notThrows` closures already repeat the path literally (closures can't reach the enclosing `local.mappingPath` on Adobe CF anyway). Inlines the string into `ExpandPath()` directly. Local verification on Lucee 7 + SQLite: global directory (wheels.tests.specs.global): 113 pass / 0 fail / 0 error reloadGlobalsSpec only: 7 pass / 0 fail / 0 error Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc | 3 +-- .../v4-0-0/command-line-tools/wheels-commands/dev-server.mdx | 2 +- .../command-line-tools/wheels-commands/dev-server.mdx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc index e45d45af24..5e3ea7a5ac 100644 --- a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc +++ b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc @@ -74,8 +74,7 @@ component extends="wheels.WheelsTest" { it("$reincludeGlobals re-evaluates the target cfm without throwing", () => { // CFML's `include` resolves via mappings, not absolute filesystem // paths — call $reincludeGlobals with the mapping-relative form. - var mappingPath = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"; - var absPath = ExpandPath(mappingPath); + var absPath = ExpandPath("/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"); FileWrite(absPath, "function fxReinclude(){return 'first';}"); $assert.notThrows(function() { application.wo.$reincludeGlobals(file = "/wheels/tests/_tmp/reloadGlobals/reinclude.cfm"); diff --git a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx index 714ea31d04..867f527190 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx @@ -114,7 +114,7 @@ wheels reload On success: `Application reloaded successfully.` in green. On failure — most often a password mismatch — it prints `Failed to reload: ` and hints at setting `RELOAD_PASSWORD` in `.env` or `config/settings.cfm`. ## Dev loop example diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx index b7202140e4..68c3782b64 100644 --- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx +++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/wheels-commands/dev-server.mdx @@ -114,7 +114,7 @@ wheels reload On success: `Application reloaded successfully.` in green. On failure — most often a password mismatch — it prints `Failed to reload: ` and hints at setting `RELOAD_PASSWORD` in `.env` or `config/settings.cfm`. ## Dev loop example From aaabbb0c07b29c98781dcb21bec09b408afcc653 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 22 May 2026 08:23:31 -0700 Subject: [PATCH 7/8] fix(events): address Reviewer A/B consensus findings (round 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc — add a test for the DateCompare != 0 branch in $globalIncludesChanged, the "developer edited an existing helper" path the PR is designed to serve. Uses the backdated-snapshot approach from Reviewer A's response (DateAdd seconds -60 on the snapshot entry) rather than Sleep(1100), so the test is deterministic across filesystems with different mtime granularities. - vendor/wheels/events/EventMethods.cfc — append application name to the wheels_reload_globals lock so concurrent ?reload=true hits from different apps on a shared Adobe CF server no longer serialize on a single global lock. Development-only and uncommon in practice, but the fix is a one-liner. Signed-off-by: Peter Amiri --- vendor/wheels/events/EventMethods.cfc | 6 ++++-- .../tests/specs/global/reloadGlobalsSpec.cfc | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/vendor/wheels/events/EventMethods.cfc b/vendor/wheels/events/EventMethods.cfc index 1514d82616..e5e80a285b 100644 --- a/vendor/wheels/events/EventMethods.cfc +++ b/vendor/wheels/events/EventMethods.cfc @@ -194,8 +194,10 @@ component extends="wheels.Global" implements="wheels.interfaces.events.EventHand ) { // Double-checked locking — two concurrent ?reload=true hits would // otherwise both pass the outer check and race on $reincludeGlobals - // against the same application.wo instance. - lock type="exclusive" name="wheels_reload_globals" timeout="5" { + // against the same application.wo instance. Lock name is + // per-application so shared Adobe CF servers running multiple + // apps don't serialize on a single global lock. + lock type="exclusive" name="wheels_reload_globals_#application.applicationName#" timeout="5" { if (application.wo.$globalIncludesChanged(snapshot = application.wheels.globalIncludesSnapshot)) { application.wo.$reincludeGlobals(); application.wheels.globalIncludesSnapshot = application.wo.$snapshotGlobalIncludes(); diff --git a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc index 5e3ea7a5ac..898781196e 100644 --- a/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc +++ b/vendor/wheels/tests/specs/global/reloadGlobalsSpec.cfc @@ -71,6 +71,20 @@ component extends="wheels.WheelsTest" { expect(ctx.g.$globalIncludesChanged(snapshot = snapshot, directory = ctx.baseDir)).toBeTrue(); }); + it("$globalIncludesChanged returns true when a tracked cfm file is modified", () => { + // Exercise the DateCompare != 0 branch — the "developer edited + // an existing helper" path the PR is designed to serve. + // Backdate the snapshot entry rather than sleeping for a fresh + // mtime, so the test is deterministic across filesystems with + // different mtime granularities (ext4 nanosecond vs APFS/HFS+ + // 1-second). + FileWrite(ctx.baseDir & "/modified.cfm", "function fxV1(){return 1;}"); + var snapshot = ctx.g.$snapshotGlobalIncludes(directory = ctx.baseDir); + var key = ListFirst(StructKeyList(snapshot)); + snapshot[key] = DateAdd("s", -60, snapshot[key]); + expect(ctx.g.$globalIncludesChanged(snapshot = snapshot, directory = ctx.baseDir)).toBeTrue(); + }); + it("$reincludeGlobals re-evaluates the target cfm without throwing", () => { // CFML's `include` resolves via mappings, not absolute filesystem // paths — call $reincludeGlobals with the mapping-relative form. From 0db188a5a5d27cd80b58939df5e0c8dd7464a00b Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 22 May 2026 08:30:58 -0700 Subject: [PATCH 8/8] docs: tighten CHANGELOG entry to distinguish detection from re-evaluation scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG.md — round-5 Reviewer A nit. Opening clause said "re-includes app/global/*.cfm" which overstates the re-evaluation scope: detection is broad (every *.cfm under app/global/) but $reincludeGlobals only re-evaluates functions.cfm and the files it transitively s. Rewording matches the round-4 dev-server.mdx tightening. Signed-off-by: Peter Amiri --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e36371d9ea..06ec85beee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo - Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773) - `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs -- Bare `?reload=true` now re-includes `app/global/*.cfm` in development when any tracked file's mtime has changed, so new helpers added to `app/global/functions.cfm` (or any file it includes) are picked up without the password-gated `applicationStop()` path. Wheels snapshots the include directory on application start and re-evaluates the include via a new `application.wo.$reincludeGlobals()` hook when `$globalIncludesChanged()` detects an added/removed/touched file. Opt out with `set(reloadOnGlobalChange=false)` in `config/settings.cfm` (issue #2792) +- Bare `?reload=true` now auto-detects changes to `app/global/*.cfm` in development and re-evaluates `app/global/functions.cfm` (plus any files it ``s) when any tracked file's mtime has changed, so new helpers added to `app/global/functions.cfm` (or any file it includes) are picked up without the password-gated `applicationStop()` path. Wheels snapshots the include directory on application start and re-evaluates the include via a new `application.wo.$reincludeGlobals()` hook when `$globalIncludesChanged()` detects an added/removed/touched file. Opt out with `set(reloadOnGlobalChange=false)` in `config/settings.cfm` (issue #2792) ----