Skip to content

Commit f0bdd14

Browse files
authored
Merge pull request #2892 from wheels-dev/release/4.0.3-to-main
Release 4.0.3
2 parents b1755b3 + 95f970f commit f0bdd14

363 files changed

Lines changed: 6951 additions & 27206 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ai/wheels/cross-engine-compatibility.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Wheels runs on multiple CFML engines (Lucee 5/6/7, Adobe CF 2018-2025, BoxLang) and databases (H2, MySQL, PostgreSQL, SQL Server, CockroachDB). Each engine has runtime differences that can cause code to pass on one engine but fail on another. This guide documents the known gotchas.
44

5+
**RustCFML (best-effort, experimental):** [RustCFML](https://github.com/RustCFML/RustCFML) — a young, JVM-free CFML interpreter written in Rust — is recognized as a first-class engine in the adapter layer (`server.coldfusion.productName == "RustCFML"``RustCFMLAdapter`), but it is NOT yet part of the CI matrix and cannot fully boot the framework today. The confirmed divergence handled in-framework is the **missing `cfcache` built-in** (the cfcache-backed cache degrades to a no-op via the adapter's `supportsCfcache()=false`). Remaining blockers are tracked upstream — chiefly an argument-scope-fidelity gap (undeclared/`argumentCollection`-forwarded named args lose their names) and no Query-of-Queries — so treat RustCFML support as in-progress.
6+
57
## Engine-Specific Gotchas
68

79
### struct.map() Collision (Lucee + Adobe)
@@ -329,6 +331,40 @@ H2 is the embedded database used by default in tests. Key differences:
329331
- Some MySQL-specific functions (e.g., `GROUP_CONCAT`) not available
330332
- Simpler locking model than production databases
331333

334+
### Auto-Derived Property Casing — `$lowerCaseColumnNames()` Adapter Capability
335+
336+
When a model declares no `property()` mappings, Wheels infers its properties from `cfdbinfo` column metadata. The reported column casing varies by database, so the adapter layer carries a capability flag — `$lowerCaseColumnNames()` on `Base.cfc` — that controls whether the derived property name keeps the reported case or is forced to lowercase. Adapters override this when their database folds unquoted identifiers to a non-meaningful default that would otherwise leak into Wheels-side property names.
337+
338+
| Database | Folding behavior | `$lowerCaseColumnNames()` | Resulting property for column `isHidden` |
339+
|----------|------------------|---------------------------|-------------------------------------------|
340+
| SQL Server, MySQL, SQLite | Preserves declared case | `false` (Base default) | `isHidden` |
341+
| PostgreSQL, CockroachDB | Folds unquoted identifiers to lowercase | `false` (Base default) | `ishidden` (database-reported) |
342+
| Oracle | Folds unquoted identifiers to UPPERCASE | `true` (override) | `ishidden` (lowercased from `ISHIDDEN`) |
343+
| H2 | Folds unquoted identifiers to UPPERCASE | `true` (override) | `ishidden` (lowercased from `ISHIDDEN`) |
344+
345+
```cfm
346+
// vendor/wheels/databaseAdapters/Base.cfc
347+
public boolean function $lowerCaseColumnNames() {
348+
return false; // preserve reported case by default
349+
}
350+
351+
// vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc — override
352+
public boolean function $lowerCaseColumnNames() {
353+
return true; // ISHIDDEN → ishidden (Oracle folds to UPPERCASE)
354+
}
355+
356+
// vendor/wheels/databaseAdapters/H2/H2Model.cfc — override
357+
public boolean function $lowerCaseColumnNames() {
358+
return true; // ISHIDDEN → ishidden (H2 folds to UPPERCASE)
359+
}
360+
```
361+
362+
**When adding a new database adapter**: check whether the database's unquoted-identifier folding rule produces case the Wheels developer actually declared. If it folds to UPPERCASE (Oracle/H2 family), override `$lowerCaseColumnNames()` to return `true`. If it preserves case (SQL Server/MySQL/SQLite) or folds to lowercase (PostgreSQL/CockroachDB), keep the Base default — the reported name is already the right property name.
363+
364+
**Explicit `property(name=..., column=...)` declarations bypass this entirely** — they always win, regardless of the adapter flag. The capability only affects the auto-derived path.
365+
366+
**Reference**: `vendor/wheels/Model.cfc` (auto-derivation site), `vendor/wheels/databaseAdapters/Base.cfc::$lowerCaseColumnNames`, regression spec `vendor/wheels/tests/specs/model/propertyCasePreservationSpec.cfc`, [#2852](https://github.com/wheels-dev/wheels/pull/2852).
367+
332368
### Migration Date Functions
333369

334370
Use `NOW()` for cross-database compatibility in migrations:

.ai/wheels/testing/browser-testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,5 @@ bash tools/test-local.sh # skips browser specs if JARs missin
6565
- **Data URLs work for most tests** — no server needed for ~95% of DSL coverage. Full HTTP integration (cookies, form submits, redirects) needs a running fixture app; that wiring is the same as Wheels Web app bootstrap (separate server + baseUrl).
6666
- **`this.browserTestSkipped`** — when Playwright JARs aren't installed (fresh CI, clean machine), `beforeAll` sets this flag and `browserDescribe`'s hooks short-circuit. All `it`s should check `if (this.browserTestSkipped) return;` to stay green on CI.
6767
- **CI runs browser tests**`pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically. The base URL is resolved at instance time through a layered lookup (`this.baseUrl` → Wheels setting → JVM property `wheels.browserTest.baseUrl` → env var → CGI auto-detect → `http://localhost:8080`); per-spec `this.baseUrl` takes priority over the env var. Set `this.baseUrl` in the component pseudo-constructor (outside any function), not inside `beforeAll()``super.beforeAll()` calls `$resolveBaseUrl()` and caches the result, so a `this.baseUrl =` assignment that runs after `super.beforeAll()` is silently ignored.
68-
- **Fixture routes**`/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application.
68+
- **Fixture routes**`/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application. The `/_browser/login-as` handler is configurable: `set(browserLoginAsHandler = "AuthFixture##loginAs")` in `config/settings.cfm` substitutes that `Controller##action` at route-registration time (default is `BrowserTestLogin##create`). Env-gating is handled by `wheels.middleware.BrowserTestFixtureGuard` on the whole `/_browser` scope — custom handlers do not need to re-implement the guard. Empty string or absent setting falls back to the default. (#2830)
6969
- **Dialogs are Lucee-only**`acceptDialog`, `dismissDialog`, `dialogMessage` use `createDynamicProxy` which is Lucee-specific. Specs skip gracefully on other engines.

.ai/wheels/wheels-bot.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,23 @@ Flip the repo variable `WHEELS_BOT_ENABLED` to `false` to halt every bot workflo
3232
## Auto-fire safety net
3333

3434
The bot is permitted to chain stages (triage → research → propose-fix), and handoff fires on `*-confidence:high` OR `*-confidence:medium`. Low stays manual. Sensitive areas (security, middleware, migrations, deploy, DI, cross-engine) are caught by the propose-fix prompt's own step-4 safety net, which posts a `fix-held` marker instead of opening a PR. Reviewer A and B then critique whatever propose-fix produces, escalating to the Senior Advisor on deadlock. All bot PRs land as `--draft` and require a human approving review on `develop`.
35+
36+
## PR-prep automation (release unblocking)
37+
38+
- **Commit-message gate.** `pr.yml`'s `Validate Commit Messages` lints the
39+
**PR title** (the squash subject), not every commit — because PRs are
40+
squash-merged, intermediate commit headers don't land in `develop`; only the
41+
PR title does. Edit the title to fix a failure; the `edited` trigger re-runs
42+
the check (and `fast-test` is skipped on title-only edits). Local guard:
43+
`tools/test-commit-title.sh`.
44+
- **Freshen (`bot-freshen.yml`).** On push to develop + a 30-min backstop:
45+
behind-but-clean bot PRs are updated via non-destructive `update-branch`;
46+
DIRTY ones are dispatched to the resolver. Decision logic:
47+
`.github/scripts/freshen-decide.sh`.
48+
- **Conflict resolution (`bot-resolve-conflicts.yml` + `/resolve-conflicts`).**
49+
A deterministic classifier (`.github/scripts/classify-conflicts.sh`)
50+
auto-resolves content/docs conflicts (markdown/MDX anywhere, CHANGELOG,
51+
`.ai/`, `docs/`) and pushes; any code conflict is escalated with
52+
the `conflict:needs-human` label and a comment — never auto-resolved.
53+
- **Not automated:** merging. PRs are brought to a green, conflict-free,
54+
ready state; the maintainer performs the final squash-merge.

.claude/commands/_shared-rails.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ they are honored. Violating them is a bug — fix the prompt, not the rails.
4141
`ci`, `chore`, `revert`. **Scope is optional and unrestricted** — pick a
4242
short noun that helps a reader skim history (e.g. `model`, `web/blog`),
4343
or omit it entirely. Don't agonize over which scope is "right."
44-
- **Subject ≤ 100 chars, not ALL-CAPS.** Sentence-case is fine.
44+
- **Header ≤ 100 chars, not ALL-CAPS.** commitlint measures the WHOLE header —
45+
`type(scope): subject` including the `type(scope): ` prefix — not just the
46+
subject. A 90-char subject under a `docs(web/guides): ` prefix is a 108-char
47+
header and FAILS. Count the prefix. Sentence-case is fine.
48+
- **The PR title is the linted gate.** Because the repo squash-merges, the PR
49+
title becomes the landing commit subject and is what CI validates — make the
50+
PR title itself a valid conventional-commit header ≤ 100 chars.
4551
- **DCO sign-off required.** Every commit you author MUST end with the
4652
trailer `Signed-off-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com>`
4753
matching the configured git author identity. Use `git commit -s` (the

.claude/commands/address-review.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,22 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
2525
## Args
2626

2727
- `<pr-number>` — the PR with converged-changes markers to address
28+
- `<head-sha>` — the PR head SHA at the start of this run, captured once by
29+
the workflow and passed here. Use it verbatim as the marker SHA — it is the
30+
`<sha>` / `<sha-before>` every marker below writes (the head *before* your
31+
own commit). Don't compute the SHA yourself — re-deriving it mid-session is
32+
the #2848 race. This governs only the marker SHA; you still use `gh pr view`
33+
normally to read comments, the consensus, and the head ref name.
2834

2935
## Steps
3036

31-
1. **Idempotency + outer-loop cap.** Read PR comments via
32-
`gh pr view <pr-number> --json comments,headRefOid,headRefName`.
37+
1. **Idempotency + outer-loop cap.** Throughout this command, the marker SHA
38+
— written `<sha>` and `<sha-before>` below — is the `<head-sha>` argument
39+
you were passed; don't compute it yourself (issue #2848). Read PR comments
40+
via `gh pr view <pr-number> --json comments`.
3341
- If any comment contains
34-
`wheels-bot:address-review:<pr>:<sha>:` for the current head
35-
SHA, exit silently — already addressed at this SHA.
42+
`wheels-bot:address-review:<pr>:<head-sha>:` for the `<head-sha>`
43+
you were passed, exit silently — already addressed at this SHA.
3644
- Count comments matching `wheels-bot:address-review:<pr>:` for
3745
ANY SHA on this PR. If count ≥ 5, post:
3846

@@ -44,7 +52,7 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
4452
either the PR's scope is larger than the bot can resolve, or
4553
the reviewers are deadlocked on a design call.
4654
47-
<!-- wheels-bot:address-review:<pr>:<sha>:terminal -->
55+
<!-- wheels-bot:address-review:<pr>:<head-sha>:terminal -->
4856
```
4957
5058
and exit.
@@ -90,7 +98,7 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
9098
change. The PR's reviewer-feedback exchange is preserved above
9199
for context.
92100

93-
<!-- wheels-bot:address-held:<pr>:<sha> -->
101+
<!-- wheels-bot:address-held:<pr>:<head-sha> -->
94102
```
95103
96104
and exit.
@@ -145,17 +153,17 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
145153
SHA. Convergence loop continues until reviewers align on `approve`
146154
or the outer-loop cap (5 rounds) is reached.
147155
148-
<!-- wheels-bot:address-review:<pr>:<sha-before>:<N> -->
156+
<!-- wheels-bot:address-review:<pr>:<head-sha>:<N> -->
149157
```
150158

151159
8. **Self-check before posting.**
152160
- [ ] Branch-aware scope check passed — no files modified outside
153161
allowed paths
154162
- [ ] For `fix/bot-*`: tests re-run, output cited in the comment
155163
- [ ] Commit message is conventional, subject ≤ 100 chars
156-
- [ ] PR comment includes the marker with the correct
157-
`<sha-before>` (the head SHA at the start of this run, not after
158-
your commit)
164+
- [ ] PR comment includes the marker built from the `<head-sha>`
165+
argument (the head SHA at the start of this run, before your
166+
commit — never a value you re-derived; issue #2848)
159167
- [ ] Outer-loop count is correctly reflected in the round number
160168

161169
If any check fails, do not post; investigate and exit non-zero.

.claude/commands/advise-on-deadlock.md

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,37 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
2828
## Args
2929

3030
- `<pr-number>` — the PR with deadlocked A↔B exchange
31+
- `<head-sha>` — the commit SHA this advice runs against; the workflow
32+
resolves it once and checks it out, then passes it here. Use it verbatim as
33+
the marker SHA wherever this prompt writes `<sha>`. Don't compute the SHA
34+
yourself — re-deriving it mid-session is the #2848 race. This governs only
35+
the marker SHA: you still use `gh pr view` / `gh pr diff` normally to read
36+
the PR's comments, reviews, and diff.
3137

3238
## Steps
3339

34-
1. **Idempotency check.** Read PR comments via
35-
`gh pr view <pr-number> --json comments,headRefOid`. If any
36-
comment contains `wheels-bot:advisor:<pr>:<sha>` for the current
37-
head SHA, exit silently — already advised at this SHA.
40+
1. **Idempotency check.** Throughout this command, `<sha>` means the
41+
`<head-sha>` argument you were passed; don't compute it yourself
42+
(issue #2848). Read PR comments via
43+
`gh pr view <pr-number> --json comments`. If any comment contains
44+
`wheels-bot:advisor:<pr>:<head-sha>` for the `<head-sha>` you were
45+
passed, exit silently — already advised at this SHA.
3846

3947
2. **Confirm the deadlock.** Look for a comment containing
40-
`wheels-bot:review-b:<pr>:<sha>:terminal` for the current head
41-
SHA. That's the trigger marker. If no terminal marker is present
42-
for the current SHA, exit silently (this command shouldn't have
48+
`wheels-bot:review-b:<pr>:<head-sha>:terminal` for the `<head-sha>`
49+
you were passed. That's the trigger marker. If no terminal marker is
50+
present for `<head-sha>`, exit silently (this command shouldn't have
4351
fired).
4452

4553
3. **Read the full exchange.**
4654
- The PR diff via `gh pr diff <pr-number>`.
4755
- The PR title/body via `gh pr view <pr-number>` for original
4856
context (and the `Fixes #<issue>` link, if any — the original
4957
issue's framing matters).
50-
- All `wheels-bot[bot]` PR reviews on the current SHA: A's initial
58+
- All `wheels-bot[bot]` PR reviews on `<head-sha>`: A's initial
5159
review and any response reviews
5260
(`wheels-bot:review-a-response:`).
53-
- All `wheels-bot[bot]` PR comments on the current SHA matching
61+
- All `wheels-bot[bot]` PR comments on `<head-sha>` matching
5462
`wheels-bot:review-b:<pr>:<sha>:` — the full B critique chain in
5563
chronological order.
5664

@@ -137,14 +145,16 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
137145
<if verdict is `approve`, note that the disputed findings should be
138146
dropped and the PR is fine to merge as-is.>
139147
140-
<!-- wheels-bot:advisor:<pr>:<sha> -->
148+
<!-- wheels-bot:advisor:<pr>:<head-sha> -->
141149
<CONVERGENCE_MARKER>
142150
```
143151

144-
Where `<CONVERGENCE_MARKER>` is:
145-
- `<!-- wheels-bot:converged-approve:<pr>:<sha> -->` if verdict is
152+
Build every marker SHA from the `<head-sha>` argument — never a value
153+
re-derived during the session (issue #2848). Where `<CONVERGENCE_MARKER>`
154+
is:
155+
- `<!-- wheels-bot:converged-approve:<pr>:<head-sha> -->` if verdict is
146156
`approve`
147-
- `<!-- wheels-bot:converged-changes:<pr>:<sha> -->` if verdict is
157+
- `<!-- wheels-bot:converged-changes:<pr>:<head-sha> -->` if verdict is
148158
`changes` (triggers `bot-address-review.yml`)
149159

150160
10. **Self-check before posting.**
@@ -155,7 +165,9 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
155165
- [ ] Verdict is one of `approve` or `changes` — not "kinda
156166
mostly", not equivocal.
157167
- [ ] Convergence marker is consistent with the verdict.
158-
- [ ] Advisor marker present.
168+
- [ ] Advisor and convergence markers present and built from the
169+
`<head-sha>` argument — not a SHA re-derived during the session
170+
(issue #2848).
159171

160172
If any check fails, fix before posting. The advisor's verdict is
161173
authoritative within the convergence loop — get it right.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# /resolve-conflicts
2+
3+
Reconcile content/docs merge-conflict markers on a bot PR branch (low-risk paths only). Invoked by bot-resolve-conflicts.yml after a deterministic risk gate.
4+
5+
## Rails
6+
7+
Read `.claude/commands/_shared-rails.md` first — they apply to every step
8+
below. Highlights for this command:
9+
10+
- Use `gh` for GitHub state, `git` for the PR branch only.
11+
- **Filesystem writes are limited to the conflicted content/docs files only.**
12+
Never touch code.
13+
- Output is **a completed merge commit** — the workflow pushes after this
14+
prompt completes.
15+
16+
## Args
17+
18+
- `<pr-number>` — the PR branch with content/docs conflict markers to resolve
19+
20+
# Resolve content conflicts — PR #<pr-number>
21+
22+
You are running inside `bot-resolve-conflicts.yml`. The workflow has already
23+
merged `origin/develop` into the PR branch and a **deterministic classifier
24+
has confirmed every conflicted file is pure documentation/content**
25+
(markdown/MDX at any path, CHANGELOG, or under `.ai/` or `docs/`).
26+
27+
## Hard safety rule
28+
29+
Run this first:
30+
31+
```bash
32+
git diff --name-only --diff-filter=U
33+
```
34+
35+
Confirm EVERY listed file is in the low-risk set the upstream classifier
36+
admits — i.e. each file is a `*.md` or `*.mdx` (any path), a `CHANGELOG`
37+
file, or under `.ai/` or `docs/`. If ANY listed file falls OUTSIDE that set
38+
(any code file — `.cfc`, `.cfm`, `.js`, `.ts`, `.py`, `.sh`, `.json`, `.yml`,
39+
`.yaml` — or any other non-doc file), DO NOT resolve it. Run
40+
`git merge --abort`, post a comment saying the gate and the command disagreed
41+
(a bug), and stop. This should never happen, but never resolve a code conflict.
42+
43+
## Resolve
44+
45+
For each conflicted content file:
46+
1. Open it and read the full conflict region(s).
47+
2. Reconcile the `<<<<<<<` / `=======` / `>>>>>>>` markers by **integrating
48+
both sides' intent** — these are docs, so prose from both branches almost
49+
always belongs in the result; merge them coherently rather than picking one
50+
side and discarding the other. Remove all conflict markers.
51+
3. `git add <file>`.
52+
53+
After all files are resolved:
54+
55+
```bash
56+
git diff --name-only --diff-filter=U # must print nothing
57+
git commit --no-edit # completes the merge commit
58+
```
59+
60+
Do NOT `git push` — the workflow pushes after verifying no markers remain.
61+
Do NOT edit any file that was not in the conflicted set. Do NOT touch code.

0 commit comments

Comments
 (0)