FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Release 4.0.6 by bpamiri · Pull Request #3387 · wheels-dev/wheels · GitHub

Release 4.0.6 - #3387

Merged
cursor[bot] merged 144 commits into
mainfrom
release/4.0.6-to-main
Aug 21, 2026
Merged

Release 4.0.6#3387
cursor[bot] merged 144 commits into
mainfrom
release/4.0.6-to-main

Conversation

bpamiri commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Cut 4.0.6. See CHANGELOG.

github-actions Bot and others added 30 commits June 19, 2026 05:25
* chore: bump develop snapshot target to 4.0.6

* chore: trigger CI on auto-bump PR

Empty commit to fire pull_request:synchronize so the develop required checks
run (the bot-created auto-bump PR gets no CI under GITHUB_TOKEN).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

---------

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: bpamiri <180555+bpamiri@users.noreply.github.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es them (#3230)

Both legs were continue-on-error because 4.0.4 couldn't install on those vectors
(no arm64 apt index; RHEL Java-detection bug). 4.0.5 ships arch-independent
packages + the Java-detection fix, and both are verified live (arm64
`apt install wheels` → 4.0.5; Rocky 9 rpm starts). Remove continue-on-error so a
future regression on either vector blocks instead of passing silently.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the 4.0.4 hardening pass (security, performance, deploy,
cross-engine) and the 4.0.5 install-anywhere packaging (arm64 Linux +
daily install-smoke CI). 4.0.4 was superseded by 4.0.5 the same day, so
this single post covers both.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…skip ci]

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.5 to 4.0.6.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](form-data/form-data@v4.0.5...v4.0.6)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/commits)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…gression guard) (#3234)

* ci: add index-integrity probe to distribution install smoke (#3218 regression guard)

The daily install-smoke installs the CLI per channel and asserts the version,
but that only samples once at 14:00 UTC. The #3218 cross-channel clobber empties
the stable apt index for most of the day (a bleeding-edge snapshot wipes it
minutes after each stable publish; it's only briefly populated), so a clobber
can land outside the install legs' sample window and slip past them — and when
they do fail, "apt install failed" doesn't name the cause.

Add a fast, container-free `index-integrity` job that probes the PUBLISHED apt
and yum dist indexes directly: stable must be non-empty and name the current GA
(per arch for apt), and bleeding-edge must stay non-empty (the clobber was
bidirectional — apt-wheels#5 scopes regen per-channel to prevent both
directions). An empty/missing/stale index fails with a message that names the
regression.

Reliability: fetches use `curl --retry ... --retry-all-errors` so a transient
blip can't false-red the guardian, and all grep checks feed from a here-string
(`grep -q PAT <<<"$body"`) rather than `printf | grep -q` — under `set -o
pipefail` the latter false-fails when grep -q early-exits and SIGPIPEs the
upstream printf of a 100KB index. Verified: 3/3 green against the live indexes,
and a negative test (bogus version) correctly fails.

Refs: #3218, wheels-dev/apt-wheels#5

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

* ci: probe plain (edge) URLs + hash-consistency, mirroring apt/dnf exactly

Two corrections to the index-integrity job after finding the real #3218
failure mode is CDN edge-cache staleness, not just the origin clobber:

1. Drop the `?cb=` cache-buster. apt/dnf fetch the plain URLs that hit
   Cloudflare's edge; a cache-buster hit R2 origin instead, so the probe went
   green while real `apt install` failed on a stale edge-cached Packages.gz.
   Test the SAME plain URLs clients use.

2. Verify hash-consistency, exactly what the package managers verify: parse the
   SHA256 the (plain) Release records for each binary-<arch>/Packages.gz, fetch
   the (plain) Packages.gz, and compare — this is precisely the "File has
   unexpected size" check that failed in #3218. yum: verify the served
   primary.xml.gz matches the hash repomd references. Then gunzip and confirm
   the content lists the GA (catches an empty-but-internally-consistent index).

Binary correctness: gzip blobs are fetched to a temp file and hashed/gunzipped
from the file — never captured into a shell variable, since `$(...)` is
text-only and silently corrupts binary (strips trailing newlines, drops NULs),
which would compute a bogus hash.

Paired with apt-wheels#6 / yum-wheels#6 (set `no-store` on metadata so the edge
stops caching it). Verified against the live indexes: correctly RED on the
currently-stale apt stable amd64 edge, GREEN on arm64 / bleeding-edge / yum.

Refs: #3218, wheels-dev/apt-wheels#6, wheels-dev/yum-wheels#6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

---------

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…heelsdi services.cfm example (#3235)

The `config/services.cfm` snippet used a top-level `var di = injector();`. A bare
top-level `var` statement compile-errors on Adobe CF (#3063) — `config/services.cfm`
is an included template, not a function body, so `var` isn't valid there on Adobe.
Lucee/BoxLang tolerate it, but the example as shipped breaks for Adobe readers who
copy it. Switch to the cross-engine-safe `local.di` form (matching the DI guide and
CLAUDE.md), and update the chained references to `local.di.*`.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#3236)

Cache the mixin-integration plan (directory scan + per-file createObject + getMetaData, plus pre-resolved public-method references and a precomputed mixin-override set) once per application instead of re-paying it on every model/controller/mapper materialization. Fixes the per-instance overhead behind #3213; semantics unchanged. Includes an engine-portable guard spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167uSbSN4vZqQqL5QZfdiQm
…itions (#3239)

A custom validation `condition`/`unless` expression of the form
`this.propertyIsPresent('productid')` threw `The parameter [property] to
function [propertyIsPresent] is required but was not passed in`.

`$parseConditionArgs()` only understood named arguments (`key='val'`),
guarding on `Find("=", param)`, so a bare positional argument was silently
dropped and the target method was invoked with an empty struct.

Teach the parser to also collect positional arguments and map them onto the
target function's declared parameter names (via GetMetaData), producing a
single named-argument struct — named-arg invoke() resolves uniformly across
Lucee/Adobe/BoxLang where a numeric-keyed positional argumentCollection does
not. Verified on Lucee 7 + SQLite/MySQL and Adobe CF 2023 + MySQL.

Fixes #3238

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…skip ci]

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…vice (#3242)

The dev-UI / CLI dispatcher vendor/wheels/public/views/cli.cfm was a
935-line god template with a 44-case cfswitch whose command handlers could
not be unit-tested (the template only runs under a full HTTP request
context). This was the remaining open item of issue #2959 (review finding
P2) — the recursive-dispatch + envelope defects shipped in #2966 and the
~1MB inline-asset defect in #3012.

Extract every command into wheels.public.CliBridge — a plain, stateless
component with one method per command and an explicit command->method
allowlist. cli.cfm (now ~112 lines) builds the response envelope preamble
(security gate, lazy migration discovery), constructs a context, checks
handles(), and calls dispatch(); the result is merged into the envelope.

Behavior-preserving: handlers are the exact former case bodies with
request.wheels.params -> arguments.params, preamble reads -> context.*, and
framework primitives (model(), get(), $cliFormatMigrationStatus(),
$cliResolveDumpPath()) reached through context.host. An unknown command
stays a silent no-op, matching the former default-less switch.

Security: dispatch() only ever invoke()s a method named in the allowlist,
and params is passed as a single named argument (never spread), so a
query-string key cannot become an arbitrary function argument.

The bridge is stateless, so one instance is cached on application.wheels
(via $cliBridge()) and shared across requests; ?reload=true re-creates it.

Verified: cli/security/migrator suites green on Lucee 7; live dispatch of
read-only + POST write-path commands + the SEC-4 mutation gate + unknown
no-op + reload all confirmed against a running server; cross-engine matrix
(Lucee 7 + Adobe 2023, SQLite) shows the cli bundle 126/126 pass with only
pre-existing unrelated failures.

Refs #2959

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lder stability (#3244)

The application template set `this.wheels.rootPath` from
`GetDirectoryFromPath(GetBaseTemplatePath())` — the directory of whatever file
was originally requested. When a request bootstraps under a subfolder (e.g. the
web test runner), the base template is not the public front controller, so
rootPath mis-anchors. Because rootPath seeds `this.name` via `Hash(rootPath)`
(the issue-359 shared-app-name pattern), an unstable value silently splits one
app across two application scopes — a contributor to the "reload=true fixes it"
symptom in #3025/#2887.

Anchor to `GetCurrentTemplatePath()` instead — always this Application.cfc's own
path (the public dir), stable regardless of the requested base template. The
value is identical for a normal front-controller request, so existing apps that
uncommented `Hash(rootPath)` keep the same app name. (Deliberately not the
reporter's `dir & "../"` form, which would shift the value to the app root and
change that hash for every existing user — stability, not level, is the fix.)
Same idiom already used two lines down in rocketunit_tests/Application.cfc for
`this.webrootDir`.

Applied to the `wheels new` scaffold, the demo app, both examples, and the
legacy test app. Validated: demo app reload/boot clean; onboarding harness
(wheels new -> start -> migrate -> generate -> test) green (45/0).

Refs #3025, #2887

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…skip ci]

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…a cache (#3250)

Two v4 guide gaps surfaced while reviewing GitHub discussions:

- buttonTo's input-prefixed attribute convention (inputClass/inputId/inputRel)
  was documented in v3 guides but dropped in the v4-0-0 restructure (#1352).
  Added a buttonTo entry to the view-helpers reference explaining that button
  attributes are input-prefixed because the button renders inside a <form>,
  and that siblings like linkTo take class/id directly.
- The per-model schema/column-metadata cache in application.wheels.models —
  the cache that goes stale on a live ALTER TABLE and which $clearCache()
  cannot rebuild — was undocumented (#1481). Added a "Schema and
  model-metadata cache" section to the caching guide with reload/restart
  recovery options.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ect to wheels-base-template (#3253)

The Installing with CommandBox page existed to give 2.x/3.x CommandBox
teams a clear Wheels 4 answer, but it never mentioned the slug a
migrating 2.x user actually has in muscle memory:
`box install cfwheels-base-template`. That pre-rebrand slug still
resolves on ForgeBox (76,754 installs, top 'cfwheels' search hit) but
errors on its missing `cfwheels` core dependency and leaves an
unrunnable skeleton.

Worse, the 2.x/3.x migration table claimed
`box install wheels-base-template` + `wheels-core` was 'Unchanged' —
inaccurate for a genuine 2.x user, whose slug DID change
(cfwheels-base-template -> wheels-base-template). The row now reads
'Slug changed' and a caution Aside maps the dead cfwheels- prefixed
slugs to the supported 4.x slugs (cfwheels-base-template ->
wheels-base-template, cfwheels -> wheels-core).

Refs #3198 (post-merge review finding)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Permanent named workflow at .claude/workflows/triage-discussions.js,
invokable via Workflow({name:"triage-discussions"}) and visible in
/workflows. It fetches recent GitHub Discussions, drops Announcements
and bot reports, triages each thread, then adversarially verifies
whether each proposed action is genuinely needed against current code
and open issues. Read-only — produces a verified findings report and
posts nothing.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…skip ci]

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…#3157)

* feat(storage): add storage-disk abstraction with local and S3 drivers (#3157)

Introduce a pluggable storage layer under vendor/wheels/storage: a small
uniform disk interface (put/get/exists/delete/url/signedUrl), LocalDisk and
S3Disk drivers, and a StorageManager that resolves named disks from config
(mirroring service()/model() resolution).

S3 access uses a from-scratch SigV4 signer (no AWS SDK, no JARs): presigned
expiring GET URLs plus Authorization-header signing for cfhttp put/get/delete.
The HMAC key-derivation and SHA-256 canonical hashing reuse the same
cross-engine-green primitives as wheels.auth.JwtService.

Phase 1 of #2962. The hasOneAttached model macro (Phase 2), the built-in
local-file-serving route, and the storage() controller/model mixin helper are
deferred to follow-ups.

Tests: storage spec green on Lucee 7 and Adobe CF 2023 (20/20 each); the
SigV4 presign test pins the AWS-documented test vector. Fixed a reserved-scope
shadowing bug in the spec (Anti-Pattern #11): `var url` reads the URL scope on
both engines, so `expect(url)` saw the request struct, not the return value;
renamed to `presigned`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

* fix(storage): harden S3 request failures, path encoding, and local signed-url binding

Addresses the review on #3248.

S3Disk treated a cfhttp connection failure as success. cfhttp does not set
throwOnError, so a DNS/connection error returns a non-numeric status (e.g.
"Connection Failure") whose Val() is 0, which the `< 300` guards read as OK:
put() reported a stored object that was never written, get() returned the
error body as the object, and exists()/delete() returned true. Centralize
status interpretation in $assertSuccess and throw Wheels.Storage.RequestFailed
on any non-2xx or unparseable status (put/get/delete). exists() now returns
true on 2xx, false on 404, and throws on a connection failure / 5xx rather
than reporting a definitive absence it cannot prove. $request preserves the
raw status line for diagnostics instead of collapsing it via ListFirst.

The wire path was built from the raw key while the signer signs the
RFC3986-encoded canonical path, so keys containing spaces / reserved
characters would fail with SignatureDoesNotMatch. Expose S3Signer.encodeKey()
and route $objectPath through it so the request and public URLs are
byte-identical to what was signed (mirrors the signer's canonicalUri for
path-style vs virtual-hosted).

LocalDisk.signedUrl() appended contentDisposition outside the HMAC, so a URL
holder could alter the served disposition; it is now bound into the signed
payload via $signaturePayload (an empty disposition reproduces the legacy
"key|expires" payload, so already-issued URLs still verify). Replaced the
mislabeled "constant-time-ish" CompareNoCase, which short-circuits on the
first differing character, with a length-then-XOR-accumulate $secureEquals
that does not leak the match length through timing.

Adds 7 specs to StorageSpec.cfc: four asserting put/get/exists/delete throw on
a refused connection (pointed at 127.0.0.1:1), one for public-url encoding,
one for disposition binding, and one wrong-length-token guard. Storage bundle
is 27/27 green on Lucee 7 and Adobe 2023.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(storage): bump interface-count guard and make S3 request timeout configurable

The storage PR adds vendor/wheels/interfaces/StorageDiskInterface.cfc — the
24th interface file — which tripped the structural guard in
InterfaceCompilationSpec ("finds exactly 23 interface files", now 24). Bump the
expected count and note that it tracks the wheels.interfaces.* contract set.
The full CI suite caught this; the storage-only bundle run and the diff-based
bot review did not, since the guard lives in tests/specs/interfaces.

Also make the S3 request timeout configurable (config.timeout, default 60s)
and use a short timeout in the connection-failure specs. In CI the get()
failure spec waited the full 60s cfhttp timeout (only GET hung; PUT/HEAD/DELETE
were refused instantly), adding a minute to every core run; capping it keeps
the suite fast without changing the production default.

Storage + interfaces bundles: 30/30 green on Lucee 7 and Adobe 2023.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (#3249)

The $fromClause join builder decided whether to wrap inner joins in a
parenthesized group bound to the LEFT OUTER JOIN purely from a regex on
the include string (any `Intermediate(Target)` shape). That issue #449
HABTM/through grouping over-fired on plain belongsTo-chain nested
includes such as `include="SecondaryContact(User)"`, nesting an inner
join whose ON clause references the root FROM table — which scopes the
root out and makes MySQL reject it with "Unknown column ... in 'on
clause'". This was a regression from Wheels 2's flat joins.

Consult the association metadata instead: only set needsNesting when the
parenthesized intermediate association is a genuine hasMany/hasOne bridge
(the OUTER-joined case the grouping was designed for). A belongsTo
intermediate now falls through to the flat-join branch, restoring Wheels
2 behavior, while real HABTM/through includes still nest unchanged.

Verified on SQLite and MySQL 9.7: crudSpec 163/0/0 (incl. new #3245 +
#449 regression specs) and hasManyShortcutSpec 13/0/0; full model suite
923/0/0 on SQLite.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…SQL properties (#3254)

* feat(model): add includeCalculated to additively opt in select=false SQL properties

Adds an `includeCalculated` argument to findAll(), findOne(), and
findByKey() that opts already-declared calculated SQL properties
(property(name=..., sql=..., select=false)) back into a single finder.

Unlike `select`, it is additive — the named calculated properties are
merged on top of the default column list inside $createSQLFieldList
rather than replacing it, so the rest of the record is still returned.
This closes the inverse of the existing select=false declaration: a
property kept off the hot path can now be pulled back in per-call
without hand-listing every other column.

Unknown names throw Wheels.CalculatedPropertyNotFound in
development/testing and are ignored in production, mirroring existing
dev-only validation patterns. Pure list manipulation — no closures,
struct member functions, or other cross-engine traps.

Fixes #3252

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>

* docs: document includeCalculated finder argument for select=false SQL properties

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>

* docs: fix premature code fence in calculated-properties snippet

The includeCalculated usage example introduced a closing code fence
mid-section, ejecting the method-based calculated-property example out
of the cfm block and orphaning the trailing fence (12 -> 13 fences).
Remove the stray fence so the whole section renders as one code block;
fence parity restored to 12, matching develop.

Signed-off-by: Peter Amiri <petera@pai.com>

---------

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Peter Amiri <petera@pai.com>
…#3255)

Adds $resolveSubpathInclude() on Global.cfc to prefix the framework-relative app test-runner include with the app's resolved webPath, so /wheels/app/tests and `wheels test` work under URL-subpath / CommandBox multi-subfolder installs (item 2 of #3251). Pure helper with full unit coverage in resolveSubpathIncludeSpec.cfc, including the no-arg production call shape.

Refs #3251

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…skip ci]

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bpamiri and others added 28 commits August 4, 2026 08:00
…hat it did not throw (#3362)

$decryptCsrfCookieValue() read a cookie by trying the configured algorithm and falling back
to the legacy bare "AES" (ECB) only from its catch block. That treats "did not throw" as
"decrypted correctly", which is not true of a wrong-mode decrypt.

Decrypting an ECB ciphertext under AES/CBC/PKCS5Padding throws only when the trailing
plaintext bytes fail padding validation. They pass by chance roughly 1 time in 256 — Decrypt()
then returns garbage, the legacy fallback never runs, and a perfectly good legacy cookie reads
as corrupted. AES/GCM/NoPadding is authenticated and does reliably throw, so this only ever
affected the engines that fall back to CBC.

Impact is a reliability wart in the legacy-cookie migration window, NOT a security hole: the
caller checks IsJSON() and returns "", so it fails closed. Worth saying plainly rather than
inflating.

The fix checks the RESULT. This cookie's plaintext is always the JSON that
$generateCookieAuthenticityToken() writes, so JSON-ness is an invariant of the cookie rather
than an assumption about it, and a non-JSON result means the wrong algorithm was used — which
is exactly when the legacy attempt should still run. The legacy result is only preferred if it
too looks like the payload, so a genuinely corrupt cookie is reported exactly as before.

Found by diffing compat-matrix legs on #3351: CsrfCookieCipherSpec's legacy-AES case failed on
lucee6/sqlite. It exercises this path and its payload carries a CreateUUID(), so the coin flip
differs per run — across 326 non-empty legs it failed once. 1/326 = 0.31% observed against
~0.39% theoretical for random PKCS5 padding validity.

Testing that deterministically needed a way to reach "returned garbage without throwing" every
time rather than 1 run in 256. Probed the three behaviours on this engine:

  AES/CBC/PKCS5Padding -> THREW ("Given final block not properly padded")
  AES/CBC/NoPadding    -> RETURNED, not JSON     <- same state, every time
  AES/ECB/NoPadding    -> RETURNED, valid JSON   (same mode, decrypts fine)

so the spec forces AES/CBC/NoPadding. Red-first on a COLD server (a warm one silently reuses
the old compiled CFC): 288 pass / 1 fail / 1 error, the failure being
`Expected [false] to be true` on exactly this path.

lucee7 + sqlite, full core suite:

  develop 2c98644   4755 pass / 0 fail / 0 error
  this branch         4757 pass / 0 fail / 0 error

Exactly +2, the new specs.

Closes #3361

Signed-off-by: Peter Amiri <peter@alurium.com>
…tree (#3363)

Removes seven generated TestBox stub files that should never have been committed, and stops
the whole class of mistake at the source.

`public/testbox/system/stubs/*.cfm`, `rewrite.config` and a root `box.json` are all written
into the working tree by a local test run or dev server. None are source. None were ignored
either — so any `git add -A` after running the suite swept them into the diff.

That happened twice. I caught it in self-review across the #3334 / #3350 / #3349 / #3325 /
#3351 batch and cleaned all five branches before they merged. I then did exactly the same
thing on #3362 and did not catch it, so seven stubs are on develop.

Verified they really are regenerated rather than needed: deleting every file in
public/testbox/system/stubs/ and running the full core suite gives 4757 pass / 0 fail /
0 error, with TestBox recreating seven of them during the run. With the ignore in place a
full run now leaves `git status` completely clean, which is the property that was missing.

One file (F952D54F…) was already tracked before any of this and is deliberately left alone —
ignoring a directory does not untrack what is already in the index, and it is not mine to
remove in a cleanup commit.

Signed-off-by: Peter Amiri <peter@alurium.com>
…e SQL text (#3360)

* fix(model): read a join's type from association metadata, not from the SQL text

Follow-up to #3334, raised by wheels-bot on PR #3354 and correct.

The grouping decision that PR introduced still asked "is this an INNER join?" by searching the
generated SQL:

    FindNoCase("INNER", local.joins[local.parentPosition])

That misclassifies every table whose name contains the substring — `winners`, `spinners`,
`beginners`. A parent LEFT OUTER JOIN on `c_o_r_e_winners` reads as INNER, so its nested child
is emitted flat instead of grouped, which silently drops the parent rows #3334 exists to
preserve.

Not a regression: the pre-fix code scanned the same way, and no fixture table hits it. Worth
fixing anyway, because it is the exact anti-pattern that PR argues against — its whole thesis
is that structure should come from the association tree rather than from re-parsing the SQL
string, and this was ten lines of new code doing the opposite.

`$associationJoinsInner()` reads the association's declared `joinType`, which is the value
`$expandedAssociations` turns into the leading `INNER JOIN` / `LEFT OUTER JOIN` text in the
first place — so it is the authoritative source by construction. It falls back to the text
scan only when an entry carries no `joinType`, keeping it total for a hand-built struct.

Red-first: with the helper reverted to the bare text scan, the new spec fails
`Expected [true] to be false`.

A note on that red-check, because it nearly fooled me: run against a WARM server the reverted
build still reported green — `tools/test-local.sh` reuses a running server and the reload did
not recompile the changed CFC. The failure only appears after killing the server first. Any
red-check on this suite needs a cold start to mean anything.

lucee7 + sqlite, full core suite:

  develop 2c98644   4755 pass / 0 fail / 0 error
  this branch         4756 pass / 0 fail / 0 error

Exactly +1, the new spec.

Refs #3334

Signed-off-by: Peter Amiri <peter@alurium.com>

* test(job): scope JobClassRoundTripSpec locals with local.

Second wheels-bot review follow-up from the same batch, on PR #3358.

Every sibling in vendor/wheels/tests/specs/jobs/ declares spec variables with `local.` (e.g.
`local.bootstrapJob = new wheels.Job();` in JobRobustnessSpec). JobClassRoundTripSpec assigned
them unscoped, which runs green but leaks the writes into the spec's `variables` scope.

Purely a convention fix — no behaviour change. Folded in here rather than opened as a third
PR, since it is the same review round on the same merged batch.

lucee7 + sqlite, full core suite: 4756 pass / 0 fail / 0 error, unchanged.

Signed-off-by: Peter Amiri <peter@alurium.com>

* fix(test): keep the catch-block struct unscoped — local. breaks it on BoxLang

The compat matrix for this branch came back +24 tests on all 28 legs, which is correct, and
two NEW failures on boxlang for every database:

  Failed | throws Wheels.JobClassNotFound naming the row and the class
         | Expected [Wheels.JobClassNotFound] but received []
  Failed | throws Wheels.InvalidJobClass when the path resolves to something that is not a job
         | Expected [Wheels.InvalidJobClass] but received []

Both are mine, and both were caused by the previous commit — the `local.`-scoping convention
nit from the bot review. `thrown` is written from inside a catch block, and on BoxLang the
catch body runs under a nested `local` that is discarded on exit. Prefixing the struct made
`local.thrown.type = e.type` land on that discarded copy instead of mutating the outer struct,
so the assertion read an empty type.

Cross-engine invariant 11 already covers the scalar case. What it did not say is that the
struct workaround it recommends only works when the struct is accessed WITHOUT the prefix —
`local.state.flag = true` fails exactly like `local.X = ...`. The prefix is what breaks it,
not the assignment shape. Widened the invariant with that, plus a worked example, because
`local.`-scoping spec variables IS the house style everywhere else, which makes tidying a
catch-using spec to match an easy and completely invisible way to break it.

So the original unscoped form was correct and the "nit" was wrong. Reverted for `thrown` only
— every other variable in the file stays `local.`-scoped, since those are written from try
bodies and are unaffected — with a comment at both sites explaining why, so it does not get
tidied back.

Worth noting the failure mode: green on Lucee, green on Adobe, wrong only on BoxLang, and
silent rather than an error. Nothing local would have caught it.

lucee7 + sqlite, full core suite: 4756 pass / 0 fail / 0 error, unchanged.

Signed-off-by: Peter Amiri <peter@alurium.com>

---------

Signed-off-by: Peter Amiri <peter@alurium.com>
…ed them (#3364)

Third occurrence of the same slip, with a wrinkle worth recording.

PR #3363 untracked seven generated TestBox stubs and added the .gitignore entries that stop
them coming back. PR #3360 then merged on top and re-added all seven — its branch was cut from
2c98644, before the ignore existed, and it was committed with `git add -A` while the paths
were still untrackable-but-not-ignored. Merging the cleanup first and the older branch second
undid the cleanup.

The .gitignore is on develop now, so a branch cut from this point cannot repeat it. This
commit just finishes what #3363 started: seven removals, no other change.

Verified after committing that exactly one stub remains tracked — F952D54F…, which predates
all of this and is deliberately left alone.

The lesson is about ORDER, not just about `git add -A`: after merging a cleanup PR, every
already-open branch that predates it still carries the thing that was cleaned, and merging it
silently reverts the fix. Check the remaining branches for it before merging them, not after.

Signed-off-by: Peter Amiri <peter@alurium.com>
…t-fail check, honest summary (#3366)

* ci: compat-matrix safe-slice hardening for #3302

Three workflow-plumbing fixes in compat-matrix.yml, shippable before the
continue-on-error flip (which stays gated on PR #3365 merging plus one
green dispatch on develop):

- run-tests: per-leg zero-test guard — an HTTP 200/417 leg reporting
  totalSpecs below 4000 (suite runs ~4,700) now emits ::error:: and sets
  the leg fail flag, so a compile-wiped leg fails loudly instead of
  rendering as a pass. SOFT_FAIL_DBS is respected.
- publish-results: fail_on: nothing on publish-unit-test-result-action,
  so oracle soft-fail debt stops pinning a red aggregate 'Wheels Test
  Results' check to innocent dispatch SHAs. Annotations, PR comments and
  artifacts are unchanged; leg gating stays in the tests job.
- test-matrix-summary: zero-test legs render as ':warning: N tests'
  instead of a checkmark, soft-fail DB failures render as :warning:, and
  the Oracle column is annotated as soft-fail with a footnote.

The rustcfml job is untouched (intentionally informational).

Refs #3302

Signed-off-by: Peter Amiri <petera@pai.com>

* ci: align per-engine summary with the zero-test guard

Review follow-up for the #3302 safe slice: the per-engine step summary
still rendered a compile-wiped leg (0 failures, 0 testcases) as a pass
while the run-tests guard in the same job failed it with ::error::.
Read totalSpecs alongside the failure count and render sub-floor legs
as ':warning: N tests (zero-test guard)', mirroring the matrix grid.

Refs #3302

Signed-off-by: Peter Amiri <petera@pai.com>

---------

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
…ForgeBox guidance (#3367)

channels.mdx and server-sent-events.mdx still denied that WebSocket support
exists, pointing readers at roadmap issue #2962. The first-party
wheels-websockets package shipped at v0.2.0 and is installable via
'wheels packages add wheels-websockets' (RustCFML native since v0.1.0,
Lucee 6.2+ verified live, Lucee 7 pending 7.0.2.7+ plus a jakarta-compatible
extension release, Adobe/BoxLang demand-gated with graceful SSE fallback).
Both pages now document the package instead of denying it, matching
why-wheels.mdx.

installing-with-commandbox.mdx claimed cfwheels-base-template 'still resolves
on ForgeBox but errors on its missing cfwheels core dependency' — the slug is
now fully unlisted and box install fails with 'entry slug invalid or does not
exist'. Also adds the legacy cfwheels-cli slug to the old-to-new mapping,
pointing readers at the standalone wheels CLI.

Refs #3292, Refs #3182

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#3346) (#3368)

* feat(model): allow select() and friends to start a query-builder chain (#3346)

Extends the chain-entry dispatch list in model onMissingMethod with
select, include, group, distinct, and forUpdate so every QueryBuilder
method can start a chain directly on the model class, matching where()
and orderBy(). Adds forUpdate to the ScopeChain builder-transition list
for parity. User-defined scopes keep precedence in both dispatchers.

Specs pin the issue's exact example (select().where().get()), the
returned columnList, include/group/distinct smoke coverage, and the
scope-to-forUpdate transition. Guide and CLAUDE.md quick reference now
document the entry-position builder methods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

* fix(model): correct precedence note on the builder chain-entry dispatch list

The comment added in the #3346 change claimed dynamic finders and
association setters take precedence over the chain-entry list, but both
branches dispatch AFTER it in onMissingMethod — only user-defined scopes
and enum checkers run first. Comment-only change; no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

---------

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Adds an upgrading-section page documenting the measured 4.0.x per-instance
mixin-integration regression reported in #3213, the #3236 fix and its measured
deltas (Lucee 7 + SQLite), the June 2026 profiling baseline, and how upgraders
get the fix (bleeding-edge until the v4.0.6 cut). Registered in the v4-0-0
sidebar to avoid the orphan-page trap.

Refs #3213

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Ports the v3-0-0 page into the v4-0-0 tree, corrected for 4.0.x: the
super<name> convention now applies to models AND controllers/view
helpers (parity landed with the #3325 fix, PR #3357). Documents
placement conventions (single controller, app/controllers/Controller.cfc,
app/views/helpers.cfm), the superLinkTo(argumentCollection=arguments)
delegation pattern pinned by SuperOverrideSpec.cfc, the dataConfirm
data-* pass-through alternative for the jsconfirm use case, and a
version callout with the CreateObject workaround for 4.0.x builds
predating the fix. Explicitly notes mapper internals are not an
override surface.

Registers the page in the v4-0-0 sidebar and section index, and adds a
version-scope note to the v3-0-0 page (model-only in 3.x).

Fixes #3343

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…3025) (#3373)

The web test runner (vendor/wheels/tests/runner.cfm) swaps the LIVE application.wheels
struct for test configuration (backup in application.$$$wheels) and restores it at the
end of the run. Overlapping test requests clobbered each other's backup, which could
restore TEST config as the live config until the next reload=true, or error the parent
run's restore outright (key [$$$WHEELS] doesn't exist).

Stage-1 slice of the issue-3025 analysis (the titled request-scoped overlay was ruled
infeasible in the 2026-06-22 adversarial analysis):

- Wrap the swap->run->restore window in an exclusive named cflock
  ('wheelsTestRunner_<applicationName>', timeout 1800s, throwOnTimeout), following the
  swap-under-lock precedent in migrator/TenantMigrator.cfc.
- Re-entrancy guard: ParallelRunner partition sub-requests re-enter runner.cfm via fresh
  top-level HTTP GETs while the parent holds the swap. They detect the already-applied
  swap (application.$$$wheels exists) and skip BOTH the swap and the shared lock (a
  unique per-request lock-name suffix), so parallel mode cannot deadlock.
- The restore now runs in a finally block, and only the request that created the backup
  restores it — an erroring suite no longer leaves test config live. No loops in the
  finally block (Lucee 7 miscompiles local-scoped loops in finally).
- Delete the orphaned legacy RocketUnit runner twin vendor/wheels/rocketunit_tests/Test.cfc
  (nothing loads it; the Global.cfc scan-exclusion entry stays). The active CLI/legacy
  chain via wheels.Test and $restoreTestRunnerApplicationScope is untouched.

New spec vendor/wheels/tests/specs/internal/TestRunnerSwapLockSpec.cfc: structural guard
for the lock acquisition + re-entrancy detection, and a behavioral nested-run test that
reproduced the backup clobber before the fix.

Residual gap (intentional, refs #3025): this serializes test-vs-test only. A normal
request concurrent with a test run still reads swapped config; true isolation is
deferred to a separate-application-context design.

Refs #3025

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
… un-hides (#3345) (#3370)

* fix(events): render debug bar restore button outside the container it un-hides (#3345)

The minimized 'Debug' restore button (##wdb-minimized) was nested inside
the ##wheels-debugbar container, which wdbMinimize() hides with
display:none — a descendant of a display:none element never renders, so
the button could not appear and the bar stayed hidden for the whole
browser session (sessionStorage re-minimizes on every load).

Move the ##wdb-minimized block after the container's closing div, still
inside the cfsavecontent so the whitespace-collapse ReReplace applies.
It is independently position:fixed, so as a sibling it stays visible
when the container is hidden. The debugbar.js script include moves
below both elements because its load-time wdbMinimize() re-invocation
does getElementById('wdb-minimized') and must find it in the DOM.

New structural spec asserts id="wdb-minimized" appears only after the
div balance for ##wheels-debugbar returns to zero (sibling, not
descendant).

Signed-off-by: Peter Amiri <petera@pai.com>

* fix(events): isolate the relocated restore button from host-page CSS

As a sibling of ##wheels-debugbar the ##wdb-minimized button no longer sits
under the container's all:initial isolation or its font stack, so the host
app's global CSS bled in (font-family:inherit resolved to the app's body
font — verified live: the demo app's Lato replaced the system stack).
Mirror the container's guard on the sibling: all:initial first, then its
own declarations plus the same font-family. The JS display toggling still
wins because style.display updates in place after the all shorthand.

Live-verified on Lucee 7: minimize/restore cycle, sessionStorage persistence
across reload, button at bottom-right 8px/8px in the system font. Full core
suite 4759 passed / 0 failed / 0 errors.

Signed-off-by: Peter Amiri <petera@pai.com>

---------

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
The debug bar's reload link was built from raw cgi.script_name, so under a
subfolder (subpath) deployment it emitted /myapp/public/index.cfm/... links
that the user's rewrite rules do not route. Compose the base from the
resolved webPath plus the front-controller filename instead — the same
idiom urlFor() uses — extracted into the unit-tested $buildDebugReloadUrl()
helper in Global.cfc (public $-prefixed per cross-engine invariant 7).

The helper preserves the previous behavior exactly for root installs
(rewriting on and off, pinned byte-for-byte in the spec), keeps the
request.cgi.path_info vs cgi.path_info branch, the rewriteFile strip, and
the reload-param scrub, and falls back to the raw script name when webPath
is not resolved yet (early boot/error paths).

The CFML error page's displayed URL had the same class of defect and now
uses the same webPath composition (plus HTML-encoding of the base).

Fixes #3344

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
)

The two forUpdate() chain-entry specs added by PR #3368 used .count() as
the terminal. Postgres and CockroachDB reject aggregate functions combined
with FOR UPDATE, so every postgres/cockroachdb leg in the compat matrix
failed with 'FOR UPDATE is not allowed with aggregate functions'. The specs
pin chain-entry dispatch, not locking semantics, so a non-aggregate .get()
terminal asserts the same behavior on every engine.

Verified locally on lucee7 + cockroachdb (previously failing leg):
4775 pass / 0 fail / 0 error; lucee7 + sqlite: 4763 pass / 0 fail / 0 error.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Squash of PR #3365. Clears the pre-existing engine-leg debt so the compat-matrix run conclusion can be made authoritative. Validated: dispatch run 30975604292 on the branch — all 5 engine jobs green, aggregate 6 fail / 132,170 pass (oracle soft-fail set only, #2663). Refs #3302.
Removes continue-on-error from the tests job now that the debt gate is
cleared: PR #3365 merged and dispatch run 30976993285 on develop came back
fully green (all 5 engine jobs, aggregate 6 oracle soft-fails only, which
stay non-blocking via SOFT_FAIL_DBS under #2663). The rustcfml job keeps
its continue-on-error — it is an informational lane by design.

From now on a red engine leg turns the weekly run banner red instead of
being silently swallowed.

Signed-off-by: Peter Amiri <petera@pai.com>
Co-authored-by: Peter Amiri <petera@pai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](nodeca/js-yaml@4.3.0...4.3.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…rvive Adobe teardown

On Adobe ColdFusion 2023 the live application scope is unreliable during
applicationStop() teardown, so bare application.wo could resolve to a stale
Java String[] and throw "Element wo is undefined in a Java object of type
class [Ljava.lang.String;", erroring the whole site until a CF service
restart. onApplicationEnd() now invokes the Wheels global through the
passed-in arguments.applicationScope.wo, guarded with StructKeyExists, so a
partially reclaimed scope degrades to a no-op. Applied to the CLI app
template, the demo app, and the bundled example apps.

Refs #3379

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…Adobe CF

Add cross-engine invariant #19 to CLAUDE.md and a matching section to
.ai/wheels/cross-engine-compatibility.md: during applicationStop()
teardown on Adobe CF 2023, bare application.wo can resolve to a stale
Java String[] and throw 'Element wo is undefined...'. Route
onApplicationEnd() through arguments.applicationScope.wo instead and
guard with StructKeyExists (#3379, fixed in #3380).

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…scope guard

The #3380 structural spec only scanned the CLI template and demo
Application.cfc. The same teardown guard was applied to the bundled
example apps, so a revert there would have gone uncaught. Extend the
targets list and add a discovery check over the shipped trees so a
newly added onApplicationEnd copy cannot slip the list.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
test(cli): cover every shipped onApplicationEnd handler for #3379
build(deps-dev): Bump js-yaml from 4.3.0 to 4.3.1
…er/testui

build(deps-dev): Bump ip-address from 10.2.0 to 10.4.0 in /tools/docker/testui
* fix: advertise wheels packages add on docs, help, and packages site

LuCLI intercepts `packages install` before Module.cfc, so user-facing
copy, --help, and the packages website now lead with `add`. The Basecoat
chapter also copies the showcase from vendor/ after add, not GitHub.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* chore(web): refresh packages-site visual baselines for add verb

The packages index and wheels-sentry canaries now screenshot
`wheels packages add` instead of the intercepted `install` verb.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
)

* fix(test): isolate the web runner in a separate application scope (#3374)

Bind test-runner, TestClient, and browser requests to
<this.name>_wheelsTest so the live application.wheels is never swapped.
Keep the #3373 named lock as a fallback for apps without the
Application.cfc include.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* fix(test): avoid reserved client scope in $testClient (#3374)

Rename the $testClient local so Lucee no longer throws
"client scope is not enabled" on every TestClient call.
Resolve Application.cfc paths via the /config mapping
instead of walking GetDirectoryFromPath on a directory.
Map `internal` in tools/test-local.sh.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* fix(test): clear controller caches after the isolated-app path swap

Isolated-app boot mounts browser-fixture controllers on
controllerPath. A later lookup of controller("wheels") then
falls through to that last-path Controller.cfc stub, which
has no mixins. Disable the fixture remount during the swap,
clear controller/file caches, and point interface specs at
Test.cfc.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* refactor(global): decompose Global.cfc into focused includes

Split the 4.8k-line Global.cfc monolith into vendor/wheels/global/*.cfm
collaborators included at component-body scope so inheritance stays
byte-identical (no per-instance mixin copy). Adobe getMetaData() does
not enumerate includes, so protected-method discovery and Mapper
integration now also scan the include files.

Closes #3241

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* fix(global): wrap Global includes in cfscript so they compile

An include is tag-context, so bare script leaked as response text
and the app failed to boot. Match /app/global/functions.cfm.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* fix(global): restore closing brace on last Global include function

The splitter treated the last function's closing brace as the
component closer and dropped it, so lifecycle.cfm failed to compile.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* docs(changelog): add fragment for Global.cfc decomposition

Repo DoD requires changelog.d even for internal refactors.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* fix(global): keep $include helpers on Global.cfc

Lucee compiles functions from component-body includes as UDFs of the
include template. include inside those UDFs resolves against
vendor/wheels/global/ and skips /app mappings, so onAbort's
$include of onabort.cfm looked under the webroot and every abort
became HTTP 500 (LuCLI + smokes).

Co-authored-by: Peter Amiri <peter@alurium.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>

* fix(global): pin abort-guard depth to Global.cfc path

$abortInvalidRequest compared GetBaseTemplatePath() to
GetCurrentTemplatePath(). After the include split, Lucee reports the
mapping-absolute include (/wheels/global/request.cfm) as the current
template — fewer segments than public/index.cfm — so every front-
controller request looked invalid, hit onmissingtemplate, and 500'd
onAbort. Measure depth against ExpandPath("/wheels/Global.cfc"), the
same filesystem path the pre-split method used.

Co-authored-by: Peter Amiri <peter@alurium.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>

* fix(global): resolve event includes via /app mapping

Application.cfc concatenated "../../" onto eventPath (/app/events),
so $include received ../../../app/events/onabort.cfm. After the
include split that missed (public/app/events/...) and 500'd onAbort
— LuCLI 1 fail / 4 error, plus onapplicationend in Lucee smokes.

$resolveGlobalIncludeTemplate() collapses a leading ../ chain as if
the include lived on Global.cfc, and shipped Application.cfc copies
now pass eventPath through unprefixed.

Co-authored-by: Peter Amiri <peter@alurium.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>

* fix(global): hoist include resolve for Adobe reload teardown

A function call inside include "#...#" made Adobe CF 2023 re-evaluate
Global.cfc's component-body includes during applicationStop(). /wheels
mappings are already gone, so authorized ?reload=true returned 500
instead of 302. Resolve to a local first, then include that local.

Also drop an unescaped hash from GlobalSurfaceIdentitySpec's it() title
so the LuCLI core suite can compile the bundle.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* fix(test): escape eventPath hashes in GlobalSurfaceIdentitySpec

The it() title interpolates #eventPath# at compile time, so the LuCLI
core suite errors with "variable [EVENTPATH] doesn't exist" and the
bundle never runs. Escape as ##eventPath##.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* docs(test): note why GlobalSurfaceIdentitySpec escapes hashes

A raw #eventPath# in the it() title interpolates at compile time and
the LuCLI bundle never loads. Keep the title escaped.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

* fix(global): fall back when Adobe teardown drops /wheels mappings

Authorized reload still 500ed after the resolve hoist. Adobe logs:

  Could not find the included template /wheels/global/locking.cfm
  sequence: public/index.cfm, line: 191

applicationStop() clears THIS.mappings, then onApplicationEnd calls
$include and Adobe re-evaluates Global.cfc's component-body includes.
The hoist never touched those mapping-absolute includes.

Keep the includes in the component body (method-body includes do not
become component methods) and fall back to a CFC-relative path, then a
public/index.cfm-relative path. $include uses the same fallbacks so
onapplicationend.cfm can load after /app is gone too.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>

---------

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Pin-only smoke of develop against cybersonic/LuCLI 0.6.1.
Does not address #2963. Do not merge until Peter says so.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Promote the 48 pending changelog.d fragments into the [4.0.6] release
section via tools/changelog-promote.sh, and clear the fragment folder.
Done on develop first (per the release playbook) so develop's
[Unreleased] does not go stale and force a back-port after the cut.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
cursor Bot merged commit 5928131 into main Aug 21, 2026
12 checks passed
cursor Bot deleted the release/4.0.6-to-main branch August 21, 2026 00:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL