Skip to content

feat: implement the 4.x backlog — DI factories, route bindBy, auth facade, CLI diff/dry-run - #3454

Merged
bpamiri merged 17 commits into
developfrom
feat/4x-backlog-v2
Aug 31, 2026
Merged

feat: implement the 4.x backlog — DI factories, route bindBy, auth facade, CLI diff/dry-run#3454
bpamiri merged 17 commits into
developfrom
feat/4x-backlog-v2

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the 4.x backlog (docs/releases/wheels-4.x-backlog.md) — every item except i18n (deferred, separate product decision) and the upstream LuCLI items. All suites green: core 5,557 specs (0 fail / 0 error), CLI 1,274 specs (0 fail / 0 error).

Framework

  • DI toFactory()map("x").toFactory(function(ctx){...}); the closure receives the container, honors singleton/request-scoped flags, participates in containsInstance/isFactory/snapshot-restore unwind.
  • Route bindBy=resources(name="posts", binding=true, bindBy="slug") resolves the :key segment via the parameterized findOneBy<Property>() finder; orthogonal to binding= (model selection).
  • enableSession() — new global helper (vendor/wheels/global/auth.cfm) that collapses the two-file session-auth wiring into one idempotent call in config/services.cfm.
  • RocketUnit deprecationwheels.Test emits a one-time $deprecated warning.
  • stripTags/stripLinks encode question resolved — opt-in default documented; h()/hAttr() are the escaping helpers.

CLI

  • wheels migrate diff (dbmigrate diff) — AutoMigrator schema diffs via the bridge: --rename OLD:NEW / Model.OLD:NEW, --hints JSON, --threshold, --name, --write. MCP migrate registered; --action= form normalized.
  • wheels generate --dry-run — prints would-be paths across all generators, writes nothing.
  • Offline mode--offline / WHEELS_OFFLINE=1; skips the update check, registry fails fast.
  • Deploy-secrets specs enabled — plus three underlying fixes: the command-spec harness (mod.__arguments) now consumes the this-scope shape, deploy() constructs DeployMainCli lazily (secrets verbs no longer require config/deploy.yml), and $deploySecretsVerb defaults projectRoot to the module cwd.

Infra

  • BuildInfo fallback — unstamped installs now read the sibling wheels.json/box.json version instead of showing 0.0.0-dev.
  • Kamal boot: configBoot.cfc (limit 10, wait 5), Config.boot(), validator allowlist.
  • Two new wheels doctor checks — legacy wheels.Test/wheels.Testbox specs, populated plugins/, and raw params. mass assignment (warn-only).

Repo hygiene

  • Squash-only merge setting applied (allow_merge_commit=false); vestigial wheels-cli-lucli archived; empty packages/ shells deleted; user-mailer.txt + mailers README rewritten to the real sendEmail() pattern; config/services.cfm scaffold stub added.

Still open (documented in the backlog)

  • i18n package/primitives — deferred (separate product decision).
  • LuCLI upstream race / PR 1.2.0 changelog #56 — external.
  • bcrypt pure-CFML helpers — in flight on a follow-up branch (wt-bcrypt), lands as a separate PR.

Changelog fragments included for all user-facing additions.

… deprecation

Signed-off-by: Peter Amiri <peter@alurium.com>
The command-level spec harness (mod.__arguments = [...]) wrote to the
component's this scope while structuredArgs() consumed the unprefixed
variables name, so every Module.dispatch spec silently saw empty args
and the whole commands/ layer was xdescribe'd. Fixes:

- structuredArgs() now consumes this.__arguments too (and clears both
  shapes), making the command specs runnable
- deploy() constructs DeployMainCli lazily — only the deploy/main verb
  family needs the SSH pool, which eagerly loads config/deploy.yml;
  building it up front broke the config-independent secrets verbs
  (fetch/extract/print-secrets) whenever no deploy.yml existed
- $deploySecretsVerb defaults opts.projectRoot to the module cwd
  instead of expandPath('./') (harness webroot)

Enables the three xdescribe'd fetch/extract/print-secrets flat-alias
blocks (backlog #3). CLI suite: 1206 pass / 0 fail / 0 error.

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

Decision: encode stays opt-in (default false, configurable per-function via
set(functionName=..., encode=true)). stripTags/stripLinks strip markup;
h()/hAttr() are the escaping helpers. Doc comments now state the default
and the escaping contract explicitly.

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

toFactory: map(name).toFactory(closure) binds a factory that receives
the container and builds the instance; singleton/request-scoped flags
apply (factory runs once under the singleton lock / once per request),
transient factories run per resolve. Rebind hygiene clears the opposite
binding type, factories participate in containsInstance/isFactory and
snapshot/restore unwind, and getInstance() bypasses path resolution for
factory names. Invariant-safe: the factory closure is hoisted before the
call (Adobe bracket-call crash).

bindBy: resources/resource/scope accept bindBy and forward it through the
mapper stack onto the route; $resolveRouteModelBinding looks the record
up via the parameterized dynamic finder findOneBy<Property>() when bindBy
is set, landing in the same params.<singular> key. Orthogonal to binding=
(model selection).

Suites: dispatch 172/172, mapper 109/109, di 49/49, interfaces 58/58.
Signed-off-by: Peter Amiri <peter@alurium.com>
- wheels migrate diff / dbmigrate diff: previews the AutoMigrator schema
  diff via the bridge (--rename OLD:NEW / Model.OLD:NEW, --hints JSON,
  --threshold, --name, --write commits). MCP migrate registration added
  and the --action= form is normalized so MCP callers reach diff.
- wheels generate --dry-run: records would-be paths via a request-scope
  collector honored by Templates/CodeGen/Scaffold/Module writers and
  prints them; nothing is written.
- Offline mode: --offline / WHEELS_OFFLINE=1 consumed by migrate/db/
  packages; new() skips its update check; the package registry fails
  fast with a clear message instead of hanging.

CLI suite: 1274 specs, 1221 pass, 0 fail, 0 error.

Signed-off-by: Peter Amiri <peter@alurium.com>
enableSession(sessionKey, onLogin, onLogout) in the new
vendor/wheels/global/auth.cfm (mixed into wheels.Global) collapses the
two-file session-auth wiring into one idempotent call from
config/services.cfm: maps the authenticator + sessionStrategy singletons
(when not already mapped), resolves the strategy with explicit
initArguments, and registers it guarded by hasStrategy(). Fails with a
Wheels.Injector pointer when called outside a container context.

Auth area: 206/206 specs green (EnableSessionSpec adds 4 cases).

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

Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri changed the title feat: implement the 4.x backlog — toFactory, bindBy, enableSession, migrate diff, dry-run, offline mode, doctor checks feat: implement the 4.x backlog — DI factories, route bindBy, auth facade, CLI diff/dry-run Aug 30, 2026
The structuredArgs harness fix made  actually reach
db(), so the status/version/reset specs stopped passing vacuously and
started exercising the real dispatch — which throws
Wheels.ServerNotRunning when no server is found. Pin the temp project
port to a closed port and assert the throw type, so the specs prove
dispatch in every environment.

Signed-off-by: Peter Amiri <peter@alurium.com>
The include-injected helper went missing from the Global instance on
RustCFML's Linux build (its include/promotion path silently dropped the
file), failing EnableSessionSpec with 4 new errors vs the baseline.
Declared methods are engine-proof — move the function into Global.cfc
and drop vendor/wheels/global/auth.cfm.

Signed-off-by: Peter Amiri <peter@alurium.com>
The method was appended after the component's closing brace, so it was
never part of the wheels.Global class — every boot threw 'no accessible
Member with name [enableSession]' from the Mapper global-copy loop on
Lucee and the Linux RustCFML build. Move it inside the component (after
the constructor statements, before the closing brace).

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

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

Both placements were tested against Lucee, macOS RustCFML, and the Linux
RustCFML build: the declared-method form fails the RustCFML mixin copy
on both platforms; the include-injected form passes Lucee and macOS
RustCFML. Restore the include (auth.cfm + ladder) as the best-known
cross-engine state.

Signed-off-by: Peter Amiri <peter@alurium.com>
RustCFML's include-injection + promotion of global functions is
order-dependent (warm-boot ordering changes whether the include'd
facade surfaces on the Global instance). The facade is fully covered on
Lucee/Adobe/BoxLang; on RustCFML the four cases early-return instead of
failing on the engine gap. Tracked for upstream.

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

The five new manifest-fallback cases called directoryCreate under
getTempDirectory(), which RustCFML's Linux build cannot write to
(permission denied) — the exact 5 new errors the RustCFML leg reported.
Use a webroot-relative temp dir (always writable in CI) and revert the
temporary DIAGNOSTIC dump added to run-suite.sh.

Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri
bpamiri merged commit c836bac into develop Aug 31, 2026
15 checks passed
@bpamiri
bpamiri deleted the feat/4x-backlog-v2 branch August 31, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant