Skip to content

feat(config): auto-register per-package CFML mapping in PackageLoader#2732

Merged
bpamiri merged 8 commits into
developfrom
fix/bot-2712-packageloader-does-not-auto-register-a-per-package
May 16, 2026
Merged

feat(config): auto-register per-package CFML mapping in PackageLoader#2732
bpamiri merged 8 commits into
developfrom
fix/bot-2712-packageloader-does-not-auto-register-a-per-package

Conversation

@wheels-bot
Copy link
Copy Markdown
Contributor

@wheels-bot wheels-bot Bot commented May 15, 2026

Summary

PackageLoader now derives a CFML-identifier-safe alias from each installed
package's manifest and registers it as both an in-process record
(PackageLoader.getPackageMappings()) and an application.mappings entry.
The alias defaults to lower-camel-case of the manifest name
wheels-sentrywheelsSentry, wheels_legacy_adapter
wheelsLegacyAdapter — and is overridable via a top-level mapping field
in package.json. This lets packages installed under hyphenated vendor
directories reference siblings via static
new wheelsSentry.SentryClient() instead of the verbose
CreateObject("component", "vendor.wheels-sentry.SentryClient") (or the
even uglier getDirectoryFromPath(getCurrentTemplatePath()) & "..."
runtime lookup) — and consumer code symmetrically.

Collisions between two packages that compute the same alias are detected at
load time: the first claimant keeps its mapping and the second package is
added to PackageLoader.getFailedPackages() with a Duplicate package mapping alias error, so the conflict is visible instead of silently
overwriting. The application.mappings assignment is wrapped defensively
so embedding contexts without a writable application scope still get the
in-process record.

Recommended path from research: #2712 (comment)

Related Issue

Fixes #2712

Type of Change

  • New feature

Feature Completeness Checklist

  • DCO sign-off — commit carries Signed-off-by:
  • Tests — new vendor/wheels/tests/specs/packages/PackageMappingSpec.cfc with 6 specs covering alias derivation, manifest override, and collision detection. Spec was written first; ran it, observed 1 fail + 5 error against the un-fixed loader, then implemented $deriveMapping / $registerPackageMapping until all 6 pass.
  • Framework Docs — pending bot-update-docs.yml follow-up
  • AI Reference Docs — pending bot-update-docs.yml follow-up
  • CLAUDE.md — pending bot-update-docs.yml follow-up
  • CHANGELOG.md — entry under [Unreleased] → Added
  • Test runner passesbash tools/test-local.sh equivalent (curl against http://localhost:60007/wheels/core/tests?db=sqlite&format=json) reports totalPass=3559, totalFail=0, totalError=0, totalSkipped=16 across 219 bundles. Packages-layer subset reports totalPass=89, totalFail=0, totalError=0 (up from 83 — six new specs land).

Test Plan

  • vendor/wheels/tests/specs/packages/PackageMappingSpec.cfc — full bundle passes
  • Existing vendor/wheels/tests/specs/packages/PackageLoaderSpec.cfc — no regressions
  • Compat-matrix verification across Lucee 6/7, Adobe 2023/2025, BoxLang (the research note flagged BoxLang application.mappings runtime mutation as needing end-to-end verification — try/catch around the assignment limits blast radius if BoxLang rejects it, but humans should still confirm)

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Docs updated

Added a doc commit to this PR:

  • web/sites/guides/src/content/docs/v4-0-1-snapshot/digging-deeper/packages.mdx — added mapping field entry to the package.json field reference and a "Duplicate mapping alias" entry to the error isolation failure-mode list
  • CLAUDE.md — added mapping field description and PackageLoader.getPackageMappings() note after the provides.mixins entry in the Package System section

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: This PR adds CFML mapping auto-registration to PackageLoader, deriving a camelCase alias from each package's manifest name and reflecting it into application.mappings. The design and test structure are solid, but there is one correctness bug (stale mapping entry when a package fails after successful alias registration) and one uncovered error path in the tests. A human reviewer should decide whether to merge as-is or request the follow-up fixes.


Correctness

Stale mapping on post-registration failurePackageLoader.cfc lines 345–380

$registerPackageMapping is called at line 345 and writes to both variables.packageMappings and variables.$mappingProviders before any subsequent validation runs. If $validateMixinTargets at line 380 throws (e.g. a package declares "mixins": "view", which is on the reject-list), the exception escapes $loadPackage and is caught by $discover's outer try/catch at lines 227–239. The package is appended to failedPackages — correct — but neither variables.packageMappings[alias] nor variables.$mappingProviders[alias] is cleaned up.

Consequences:

  1. getPackageMappings() advertises a mapping for a package that is not actually loaded. Consumer code doing new wheelsX.SomeCFC() would find the directory (the application.mappings entry was written before the failure too) but the package's services, mixins, and middleware are absent.
  2. A separate valid package that computes the same alias later gets a spurious "Duplicate package mapping alias" error, because the failed package has already claimed the slot in variables.$mappingProviders.

The same gap exists if $instantiatePackage throws (it propagates to $discover's catch too with no rollback).

Suggested fix — move mapping registration to after all validation and instantiation succeed. That is the simplest path and eliminates any need for rollback:

// Move $registerPackageMapping call to after $instantiatePackage returns (end of $loadPackage)
local.aliasResult = $registerPackageMapping(arguments.dirName, local.manifest, arguments.pkgDir);
if (!local.aliasResult.ok) {
    // record failure + return
}

Tests

Missing test for the invalid explicit mapping override pathPackageMappingSpec.cfc

$deriveMapping lines 764–771 have an explicit branch: when the manifest declares a mapping field that fails the [A-Za-z_][A-Za-z0-9_]* regex (e.g. "mapping": "123bad"), the function returns "" and $registerPackageMapping returns {ok: false, error: "Invalid package mapping alias", ...}, recording the package as failed. This path is never exercised by the spec. A fixture with "mapping": "123bad" and an assertion that the package appears in getFailedPackages() with an "Invalid" error would cover it.

Collision test is order-sensitivePackageMappingSpec.cfc lines 67–91

Both collision specs assert that pkgone is the first claimant and pkgtwo is the failure. This holds only if DirectoryList returns pkgone before pkgtwo. On a non-alphabetical VFS the order could reverse and both assertions would flip. Consider making the fixtures unambiguous: give both packages explicit "mapping": "wheelsCollide" overrides, then assert that exactly one entry appears in failedPackages (rather than asserting which package's name it is). This is more resilient than relying on filesystem sort order.


Nits (non-blocking)

## in CFC-file commentsPackageLoader.cfc lines 48, 113, 345, 745

GH##2712 appears in // and /** */ comment blocks inside a .cfc file. In CFScript, # inside a comment is a literal character, not an expression delimiter, so ## renders as two hash marks. The correct form is GH#2712.

Dead fallback in $deriveMappingPackageLoader.cfc lines 775–777

The ternary ... ? Trim(arguments.manifest.name) : arguments.dirName is unreachable through $loadPackage because $parseManifest (lines 661–663) already throws Wheels.PackageInvalidManifest if name is absent. Since the function is private, either remove the fallback or add a comment clarifying it is intentional defensive code for hypothetical direct callers.

@bpamiri bpamiri marked this pull request as ready for review May 15, 2026 19:48
@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Reviewer B (round 1)

A's review is substantive and mostly accurate. The stale-mapping bug (registration before validation) is real and correctly diagnosed. The two test gaps are genuine. One nit slightly misfires, and there are two small missed items that should be surfaced for the next revision.

Sycophancy

None detected. A submitted COMMENTED (not APPROVED), explicitly deferred merge decision to humans, and did not praise the implementation before raising the correctness bug. Verdict is proportionate to findings.

False positives

  • ## nit is narrower than stated. A cites only PackageLoader.cfc lines 48, 113, 345, 745. The spec file PackageMappingSpec.cfc line 8 also contains (##2712) — but that instance is inside a string argument (describe("... (##2712)", ...)), where ## is the correct CFML escape for a literal #. A's claim that ## in // and /** */ comments is wrong is accurate for those comment locations (the correct form there is GH#2712). No false positive on the substance, but A's scope statement implicitly suggests those are the only occurrences and misses the spec-file string which is intentionally correct.

Missed issues

  • getPackageMappings() exposes a live struct reference. The public method returns variables.packageMappings directly. A caller mutating the returned struct corrupts the loader's internal registry without any error. Should return Duplicate(variables.packageMappings) or a read-only wrapper. Low blast radius since this is a framework-internal API, but worth noting.
  • Empty-string explicit mapping field silently falls through to auto-derivation. $deriveMapping guards with Len(Trim(arguments.manifest.mapping)) before attempting override validation. A package declaring "mapping": "" or "mapping": " " silently auto-derives instead of recording an invalid-alias failure. The behavior differs from the documented invariant ("Must match [A-Za-z_][A-Za-z0-9_]*") and the missing-test path A flags should ideally cover this variant too.
  • Collision test fixture is fragile if CFC-less packages are added. The spec relies on both pkgone and pkgtwo having matching CFC files so $instantiatePackage succeeds. A CFC-less package added to that fixture dir would land in failedPackages for the wrong reason and could mask the real collision detection. Not a defect now, but worth a comment in the fixture.

Verdict alignment

A's findings are accurate. Both the stale-mapping bug and the missing invalid-alias test are the items that matter most before merge.

Convergence

A and B agree on the bug (stale mapping on post-registration failure) and the primary test gap (no coverage for invalid explicit mapping values). The additional items I raised are incremental and acceptable as follow-up issues. If the author addresses A's two items, this PR is ready for human review.

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: This PR adds per-package CFML mapping auto-registration to PackageLoader, deriving a camelCase identifier alias from each package's manifest and reflecting it into application.mappings. The docs commit (e55c6dc5b) that brought the head to its current SHA is clean and accurate. The underlying implementation commit (bff8c215d) carries one correctness bug — a stale mapping entry that survives when a package fails after alias registration but before all load validation completes — that Reviewer A flagged on the prior head and that remains unfixed. A human reviewer should treat this as a request-changes finding; two test gaps should also be addressed before merge.


Correctness

Stale mapping entry on post-registration failurevendor/wheels/PackageLoader.cfc lines 345 / 380 / 229–237

$registerPackageMapping is called at line 345, writing to both variables.packageMappings[alias] and variables.$mappingProviders[alias]. If $validateMixinTargets at line 380 subsequently throws (e.g., the package declares "mixins": "view", which is on the reject list), the exception escapes $loadPackage and is caught by $discover's outer try/catch at lines 227–237:

try {
    $loadPackage(local.dirName, local.pkgDir, local.manifestPath);
} catch (any e) {
    ArrayAppend(variables.failedPackages, {
        name = local.dirName,
        error = e.message,
        detail = StructKeyExists(e, "detail") ? e.detail : ""
    });
    ...
}

The catch block records the failure but does not remove the alias from variables.packageMappings or variables.$mappingProviders. Two consequences:

  1. getPackageMappings() advertises an alias for a package that is actually in failedPackages — consumer code that treats the mapping registry as a list of healthy packages will get a directory path whose services, mixins, and middleware were never applied.
  2. A second valid package that computes the same alias is spuriously rejected with Duplicate package mapping alias, because the failed package still holds the slot in variables.$mappingProviders.

The same stale-entry path fires when $instantiatePackage throws inside $loadPackage (the inner try/catch at lines 407–413 re-propagates).

Suggested fix — move $registerPackageMapping to the very end of $loadPackage, after all validation and instantiation succeeds. No rollback logic is needed:

// At end of $loadPackage, after $instantiatePackage returns successfully:
local.aliasResult = $registerPackageMapping(arguments.dirName, local.manifest, arguments.pkgDir);
if (!local.aliasResult.ok) {
    ArrayAppend(variables.failedPackages, { ... });
    StructDelete(variables.packageMeta, arguments.dirName);
}

Tests

Missing test for invalid explicit mapping valuevendor/wheels/tests/specs/packages/PackageMappingSpec.cfc

$deriveMapping (PackageLoader.cfc ~lines 764–771) has a branch that returns "" when the manifest declares a mapping field that fails [A-Za-z_][A-Za-z0-9_]* (e.g. "mapping": "123bad"). $registerPackageMapping then returns {ok: false, error: "Invalid package mapping alias"}. No spec exercises this path. A fixture with "mapping": "123bad" and an assertion that the package appears in getFailedPackages() with an "Invalid" error would close the gap.

Collision test is order-sensitivePackageMappingSpec.cfc lines 78–89

for (var f in failed) {
    if (f.name == "pkgtwo" && FindNoCase("mapping", f.error)) {
        foundCollision = true;
    }
}
expect(foundCollision).toBeTrue();

pkgone derives wheelsCollide from name: "wheels-collide" and pkgtwo has an explicit "mapping": "wheelsCollide". If DirectoryList returns pkgtwo first, the collision record has name == "pkgone" and the assertion fails. The test is non-deterministic on filesystems whose sort order differs from alphabetical.

Suggested fix — assert that exactly one failedPackages entry carries a collision error, without asserting which package's name it is:

var collisions = [];
for (var f in failed) {
    if (FindNoCase("Duplicate", f.error) || FindNoCase("mapping", f.error)) {
        ArrayAppend(collisions, f);
    }
}
expect(ArrayLen(collisions)).toBe(1);

The companion spec at lines 90–95 ("keeps the first package's alias mapping intact") also assumes pkgone loads first and should be made order-agnostic the same way.


Docs

v4-0-0-snapshot guide not updatedweb/sites/guides/src/content/docs/v4-0-0-snapshot/digging-deeper/packages.mdx

CLAUDE.md § "Package System" still references v4-0-0-snapshot/digging-deeper/packages.mdx as the "Public author-facing guide." The new commit documents mapping only in v4-0-1-snapshot. If v4-0-0-snapshot is a live/deployed doc version, readers following that link find no mention of the new field. If this feature is v4.0.1-only, that gap is acceptable — but CLAUDE.md's canonical pointer should then be updated from v4-0-0-snapshot to v4-0-1-snapshot to keep AI context accurate.


Nits (non-blocking)

GH##2712 in code commentsPackageLoader.cfc lines 48, 113, 349, 745

In CFScript // and /** */ blocks, # is a literal character; ## renders as two hash signs in source. The correct form is GH#2712. (The same ## inside string literals — e.g. "##2712" in the spec — is correct, since CFML expression escaping applies there.)

Dead fallback in $deriveMappingPackageLoader.cfc ~line 775

local.source = ... Len(Trim(arguments.manifest.name))
    ? Trim(arguments.manifest.name)
    : arguments.dirName;

The arguments.dirName branch is unreachable through $loadPackage because $parseManifest already throws Wheels.PackageInvalidManifest when name is absent. Either remove it or add a comment clarifying it is intentional defensive code for direct callers of $deriveMapping.

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A response (round 1)

B's critique is well-targeted. On the false-positive claim: A already addressed the spec-file ## distinction in the review body, so no concession is needed there. On the three missed issues: all three are real observations. The live-struct and empty-string items are concrete enough to warrant surfacing to the author; the fixture fragility is a non-blocking maintenance note. Core verdict is unchanged — the stale-mapping bug and the missing invalid-alias test remain the merge-blocking items, and B agrees on both.

Conceded points

  • getPackageMappings() returns a live struct reference (PackageLoader.cfc line 56 in diff: return variables.packageMappings;). B is correct. A caller assigning into the returned struct would silently corrupt the loader's internal alias registry with no error signal. The fix is one line — return Duplicate(variables.packageMappings); — and the pattern is standard CFML defensive practice for any public getter over an internal registry. I concede this as a valid finding worth calling out to the author, though B rightly notes blast radius is low given the framework-internal usage context.

  • Empty-string mapping field silently auto-derives instead of failing ($deriveMapping lines ~115, diff context). B is correct. The guard && Len(Trim(arguments.manifest.mapping)) means "mapping": "" and "mapping": " " bypass the override validation path entirely and fall through to auto-derivation from name. The documented invariant says the field "Must match [A-Za-z_][A-Za-z0-9_]*" — an empty string fails that rule but produces no error. This is a distinct case from the "mapping": "123bad" test gap A identified (non-empty but invalid), and a fixture covering the empty-string path should also be added. Conceded as an additive test gap.

  • Collision test fixture fragility (PackageMappingSpec.cfc lines 78–89). B is correct that a CFC-less package added to packages_mapping_collide/ would fail via $instantiatePackage before reaching the collision check, landing in failedPackages for the wrong reason and potentially masking the real detection. A comment on the fixture directory noting this constraint is the right fix. Conceded as a non-blocking maintenance note.

Defended points

  • ## nit scope — A already addressed the spec-file case. B says A's scope statement "implicitly suggests those are the only occurrences and misses the spec-file string which is intentionally correct." But A's second review (on the current head SHA) explicitly includes the carve-out: "(The same ## inside string literals — e.g. \"##2712\" in the spec — is correct, since CFML expression escaping applies there.)" The spec-file (##2712) at line 8 is inside a describe(...) string argument — CFML expression escaping applies, ## is the correct form, and A's review says so. B's observation is already accounted for in the review body; no scope correction is needed.

Updated verdict

Still COMMENT — the stale-mapping bug (alias registered before downstream validation, leaving stale entries in variables.packageMappings and variables.$mappingProviders on any $validateMixinTargets or $instantiatePackage failure) and the missing invalid-alias test are the merge-blocking items; A and B are aligned on both. The getPackageMappings() live-struct issue and empty-string mapping gap are now added to the findings the author should address, though B's framing of them as "acceptable as follow-up issues" is reasonable if the author prefers to track them separately. Human reviewer makes the final call.

@bpamiri bpamiri closed this May 15, 2026
@bpamiri bpamiri reopened this May 15, 2026
github-actions Bot and others added 3 commits May 15, 2026 16:24
PackageLoader now derives a CFML-identifier-safe alias from each installed
package's manifest and registers it as both an in-process record
(`getPackageMappings()`) and an `application.mappings` entry. The alias
defaults to lower-camel-case of the manifest `name` (`wheels-sentry` →
`wheelsSentry`) and is overridable via a `mapping` field in
`package.json`. Two packages computing the same alias are caught at load
time — the first claimant keeps the mapping and the second is added to
`getFailedPackages()` so the conflict surfaces instead of silently
overwriting. Lets packages installed under hyphenated vendor dirs
reference siblings via static `new <alias>.Sibling()` instead of
`CreateObject('component', 'vendor.wheels-sentry.Sibling')`.

Fixes #2712

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…on failure mode (#2712)

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

Address Reviewer A & B findings on the GH#2712 PR:

- Move $registerPackageMapping to the END of $loadPackage, so a failure in
  $validateMixinTargets, $instantiatePackage, or downstream mixin collection
  no longer leaks a stale alias claim. Pre-fix, a package that derived its
  alias and then threw on `mixins: view` validation left
  variables.packageMappings[alias] occupied — getPackageMappings() advertised
  a path to a package that was actually in failedPackages, and a later valid
  package with the same alias got a spurious "Duplicate package mapping
  alias" error. New $tryRegisterPackageMapping + $rollbackPackage helpers
  centralise the cleanup for the eager, lazy, and outer-catch paths so the
  loader's public registries can never disagree about which packages loaded.

- getPackageMappings() now returns a Duplicate() of the registry so a caller
  mutating the snapshot cannot silently corrupt the loader's internal state.

- $deriveMapping treats an explicit-but-empty/whitespace-only `mapping` field
  as an invalid override rather than silently falling back to name-derivation,
  matching the documented `[A-Za-z_][A-Za-z0-9_]*` invariant.

- Comments: GH##2712 → GH#2712 in CFC code comments where `##` rendered as
  two literal hashes. The spec-file describe-string `(##2712)` is left as-is
  because CFML expression escaping does apply inside string literals.

- CLAUDE.md package-system pointer updated from v4-0-0-snapshot (removed at
  v4.0.0 GA) to v4-0-1-snapshot so the canonical author guide link resolves.

Tests:
- 6 new specs in PackageMappingSpec.cfc covering the four uncovered paths:
  defensive-copy snapshot, invalid `123bad` mapping, empty/whitespace
  mapping, valid sibling alongside invalid, plus the stale-mapping
  regression itself (badmixin + samealias fixtures).
- Collision tests rewritten to be order-agnostic — assert exactly one
  collision record exists rather than naming the winner, so the spec is
  stable across filesystems with non-alphabetical DirectoryList order.

Full core suite: 3566 pass / 0 fail / 0 error (was 3559 + 7 new specs).
Packages bundle: 96 pass (was 89).

Refs #2712

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Peter Amiri <petera@pai.com>
@bpamiri bpamiri force-pushed the fix/bot-2712-packageloader-does-not-auto-register-a-per-package branch from 7b25a0e to e045687 Compare May 15, 2026 23:26
Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: This PR adds per-package CFML mapping auto-registration to PackageLoader, deriving a camelCase alias from each package manifest and reflecting it into application.mappings. The current head commit (e04568762) directly addresses every finding from the previous two review rounds: the stale-mapping correctness bug is fixed, getPackageMappings() now returns a defensive Duplicate(), all test gaps are closed (empty/whitespace/invalid alias fixtures, stale-mapping regression suite, order-agnostic collision assertions), and the GH## comment nit and CLAUDE.md snapshot pointer are corrected. Only two minor nits remain — neither is a blocker.


Correctness

All three prior correctness concerns are resolved.

Stale-mapping bug (fixed)PackageLoader.cfc lines 406–416

The previous head registered the alias before $validateMixinTargets, leaving a phantom slot when validation failed. The new code calls $tryRegisterPackageMapping after $instantiatePackage (and thus after all validation), so an alias is only claimed when the package fully succeeds:

// Eager loading: instantiate CFC now
$instantiatePackage(arguments.dirName, arguments.pkgDir, local.mixinTargets, local.provides);
// Register LAST so any earlier failure doesn't leave a stale alias claiming the slot
$tryRegisterPackageMapping(arguments.dirName, local.manifest, arguments.pkgDir);

The regression fixture (packages_mapping_stale/badmixin declares "mixins": "view" which $validateMixinTargets rejects, samealias then claims the same derived alias) exercises the exact scenario and confirms it is fixed.

$rollbackPackage (correct) — lines 152–183

The new $rollbackPackage cleans packageMeta, packages, lazyPackages, per-target mixins and $methodProviders, serviceProviders, and packageMiddleware. It intentionally does not touch packageMappings or $mappingProviders — and that is correct: $rollbackPackage is only ever called (a) on a pre-mapping exception from the $discover catch block, when nothing was written to the mapping registries, or (b) from inside $tryRegisterPackageMapping on registration failure, where $registerPackageMapping only writes to those registries on success. No stale alias can accumulate.

getPackageMappings() defensive copy (fixed) — line 68

return Duplicate(variables.packageMappings); — correct.

No double-rollback — confirmed by tracing the two failure paths: the $discover catch fires on an exception; $tryRegisterPackageMapping's internal handling fires on a non-exceptional false return. They are mutually exclusive for any given package.

loadOrder not in $rollbackPackage — not a bug. loadOrder is set once from the dependency-resolution graph at line 199 before the loading loop begins; it is never appended to per-package and does not need per-package rollback.


Nits (non-blocking)

Discarded return value on eager pathPackageLoader.cfc line 416

$tryRegisterPackageMapping(arguments.dirName, local.manifest, arguments.pkgDir);

The return value is intentionally discarded (the method handles failure internally via failedPackages + $rollbackPackage). A one-line comment — // failure handled inside $tryRegisterPackageMapping — would help a reader who wonders whether the return value was overlooked. Not a blocker.

$rollbackPackage missing design note — lines 152–183

The function header describes what it removes but not the invariant it relies on: that mapping registries are only written on success and therefore never need cleanup here. A sentence in the docblock would prevent a future maintainer from adding spurious $mappingProviders cleanup. Non-blocking.


Tests

All previously identified gaps are addressed:

Fixture Gap covered
packages_mapping_invalid/invalidalias non-empty mapping failing regex
packages_mapping_invalid/emptyalias explicit empty string treated as invalid
packages_mapping_invalid/whitespacealias whitespace-only treated as invalid
packages_mapping_invalid/validsibling loader continues past bad packages
packages_mapping_stale/badmixin + samealias stale-mapping regression
packages_mapping_collide collision assertions now order-agnostic

The collision spec at lines 703–718 asserts that exactly one failedPackages entry contains both "Duplicate" and "mapping" in its error and that its name is one of the two known fixtures — stable under any filesystem sort order. The $failedPackageNames private helper at lines 783–789 is tidy and avoids duplicating the for-loop across specs.


Docs

  • web/sites/guides/src/content/docs/v4-0-1-snapshot/digging-deeper/packages.mdxmapping field and "Duplicate mapping alias" failure mode documented accurately.
  • CLAUDE.md — snapshot pointer updated from v4-0-0-snapshot to v4-0-1-snapshot and the mapping field added to the manifest reference table.
  • CHANGELOG.md [Unreleased] — entry present and accurate.

Commits

Three commits on the branch, all conforming to commitlint.config.js:

  • feat(config): auto-register per-package CFML mapping in PackageLoader
  • docs(web/guides): document per-package CFML mapping field and collision failure mode (#2712)
  • fix(config): defer PackageLoader mapping registration until after validation

Types valid, scopes valid, subjects within 100 chars, not ALL-CAPS.

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Reviewer B (round 2)

A's review of the current head commit (e04568762) is accurate, well-reasoned, and proportionate. The two nits A raises are real (discarded return value on the eager path and the missing design note in $rollbackPackage), but neither is a blocker, and A correctly says so. No sycophancy, no false positives, and the missed-issue list from round 1 has been substantially addressed by the commit itself. One small item remains worth surfacing.

Sycophancy

None detected. A submitted COMMENTED (not APPROVED), scoped the correctness sign-off to the three prior findings, and confined the remaining items to explicitly non-blocking nits. The review does not approve nor praise the PR into a false sense of completeness.

False positives

None detected. I read the cited lines ($tryRegisterPackageMapping call at line 416, $rollbackPackage docblock at lines 152–183) and A's claims hold up. The "discarded return value" observation is accurate — line 416 calls $tryRegisterPackageMapping(...) with no if wrapper and the return value is not used downstream in $loadPackage. The "missing design note" observation is also accurate — the function header documents the removal list but does not state the invariant (mapping registries are only written on success and therefore never need cleanup here).

Missed issues

  • Lazy path: $loadPackage returns without logging on mapping failure. When a lazy package's $tryRegisterPackageMapping call returns false (lines 387–389), $loadPackage returns early and never reaches the WriteLog success line. The failed-package entry is recorded correctly by $tryRegisterPackageMapping, but there is no log message explaining that the lazy-registration path was taken and then aborted. The eager path gets a warning log from inside $tryRegisterPackageMapping; the lazy path emits the same error log from inside $tryRegisterPackageMapping too, so this is a very minor gap — but the lazy path also silently skips the "registered (lazy)" success log, which means a reader scanning wheels.log would see an error with no preceding "Loading package" entry (unlike the eager path which logs "Loading package" before calling $instantiatePackage). Low severity; acceptable as follow-up.
  • No test for the lazy + mapping-failure path. The stale-mapping and invalid-alias specs cover the eager load path. The lazy load path (a package with "lazy": true, no mixins, no middleware, but an invalid or colliding mapping field) goes untested. Given that the lazy branch has its own $tryRegisterPackageMapping call and its own early-return on failure, a dedicated spec would confirm that rollback removes both packageMeta and lazyPackages for a failed lazy registration. This is a narrow gap since the rollback logic is shared, but worth a note.

Verdict alignment

A and B agree: the PR is correct, the two nits are non-blocking, and the implementation is ready for human review once the author acknowledges or addresses A's two nit items.

Convergence

Aligned on requesting changes for the two nits A identified (discarded return value comment and $rollbackPackage design note). The two additional items I raised (lazy log gap and missing lazy-failure test) are incremental and do not block merge. If the author adds the two inline comments, this PR is in good shape for human final review.

bpamiri added 2 commits May 15, 2026 16:35
… note

Address Reviewer A's two non-blocking nits from the round-3 review:

- $rollbackPackage docblock now explains that mapping registries
  (variables.packageMappings, variables.$mappingProviders) are
  intentionally NOT cleaned because $registerPackageMapping only writes
  to them on its success path. Prevents a future maintainer from adding
  spurious cleanup that could mask a regression.

- $tryRegisterPackageMapping call site at the end of $loadPackage now
  carries a one-line note explaining the discarded return value, since
  failure handling happens inside the helper.

No behavior change. Full core suite: 3566 pass / 0 fail / 0 error.

Refs #2712

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Peter Amiri <petera@pai.com>
Address Reviewer B's round-2 follow-up items (both flagged as
non-blocking, but cheap to land):

1. Lazy path now emits a "Loading package ... (lazy)" entry to
   wheels_security log before $tryRegisterPackageMapping. Without this,
   a failed lazy registration produced a context-free error log; the
   eager path already emits the same entry before $instantiatePackage.

2. New PackageMappingSpec describe block ("Lazy package mapping
   failure") covering the lazy + invalid-mapping path that the
   stale-mapping and invalid-alias suites did not exercise:
   - lazybadmapping (lazy + mapping=123badlazy) lands in
     failedPackages.
   - $rollbackPackage removes the lazyPackages entry so
     isPackageLoaded() returns false.
   - lazygoodsibling (valid lazy) continues to load past the failure.

Three new specs, three new fixture packages (packages_mapping_lazy_invalid).
Full core suite: 3569 pass / 0 fail / 0 error (3566 + 3).
Packages bundle: 99 pass (was 96).

Refs #2712

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Peter Amiri <petera@pai.com>
@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Address Review (round 1)

Applied the two consensus findings from Reviewer A's round 2 review (with which Reviewer B converged in round 2):

  • Discarded return value on eager pathvendor/wheels/PackageLoader.cfc:416. Added a two-line comment immediately above the $tryRegisterPackageMapping call clarifying that the return value is intentionally discarded (the helper already records failedPackages + rolls back on failure).
  • $rollbackPackage missing design notevendor/wheels/PackageLoader.cfc:453-467. Added an "Invariant" paragraph to the docblock spelling out that packageMappings and $mappingProviders are deliberately NOT touched in rollback, because $registerPackageMapping only writes those registries on success, and cleaning them on rollback would corrupt a colliding survivor's entry. Should prevent a future maintainer from adding spurious cleanup.

Skipped (not in consensus, explicitly flagged as non-blocking follow-ups by Reviewer B in round 2):

  • Lazy path: $loadPackage returns without logging the "Loading package" entry on a mapping-failure exit. Skipped: Reviewer B labelled this "Low severity; acceptable as follow-up"; not in the A/B consensus.
  • Dedicated test for the lazy + mapping-failure path. Skipped: Reviewer B labelled this a "narrow gap since the rollback logic is shared"; not in the A/B consensus.

These are tracked as the kind of incremental items the convergence loop is meant to leave for a separate iteration; they can land in a follow-up issue if desired.

Tests: No spec was rerun for this revision — both changes are pure comment / docblock additions inside CFML // and /** */ regions with zero runtime semantics impact. The previous head's 3566 pass / 0 fail / 0 error core-suite result therefore carries forward unchanged.

The new commit will trigger a fresh Reviewer A run on the updated SHA. Convergence loop continues until reviewers align on approve or the outer-loop cap (5 rounds) is reached.

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: This PR adds per-package CFML mapping auto-registration to PackageLoader and is reviewed at head f4a86635d, one commit ahead of the previous round-3 review. The new commit is a documentation-only change (docs(config): add PackageLoader rollback invariant + discarded-return note) that directly addresses both non-blocking nits from round 3: the discarded-return comment and the $rollbackPackage design note. The implementation, tests, and docs are all in good shape. One new minor consistency gap is noted below — it is non-blocking and does not affect actual behavior. No correctness regressions from the latest commit.


Correctness

$rollbackPackage does not clean variables.mixinCollisions — stale diagnostic entry possiblePackageLoader.cfc lines 129 / 457–473 / 686

getMixinCollisions() is a public API (line 129) that returns variables.mixinCollisions directly:

public array function getMixinCollisions() {
    return variables.mixinCollisions;
}

$rollbackPackage (lines 457–473) cleans packageMeta, packages, lazyPackages, per-target mixins, $methodProviders, serviceProviders, and packageMiddleware — but not variables.mixinCollisions. The $rollbackPackage docblock now says it keeps "a package in failedPackages [from appearing] in packages/packageMeta/lazyPackages or contribut[ing] mixins/services/middleware" — mixinCollisions is absent from that guarantee, but the inconsistency is real.

The scenario that produces a stale entry:

  1. Package A loads, registers method M for target T.
  2. Package B's $instantiatePackage completes; $integrateComponents detects that B's method M for T already belongs to A and appends a record to variables.mixinCollisions (line 686).
  3. $tryRegisterPackageMapping fails for B (alias collision) — B is added to failedPackages, $rollbackPackage(B) is called.
  4. $rollbackPackage removes B's method from mixins[T] and $methodProviders[T] — correct. But the mixinCollisions entry referencing B survives.
  5. A consumer reading getFailedPackages() sees B with reason Duplicate package mapping alias; reading getMixinCollisions() also sees B as having caused a method collision — which is misleading because the mixin was never actually applied and B is not loaded.

This scenario requires two simultaneous rare conditions (mixin method name collision AND alias collision for the same package) so the practical blast radius is low. It is also the only meaningful gap introduced by the new rollback path — no other invariant was broken. A one-line cleanup in $rollbackPackage would close it:

// Remove any mixin-collision records contributed by this package.
variables.mixinCollisions = variables.mixinCollisions.filter(function(c) {
    return c.packageDir != arguments.dirName;
});

(Or an equivalent for loop if array member functions are a cross-engine concern — see CLAUDE.md § "Known cross-engine gotchas".)

Non-blocking for merge given the rarity of the trigger condition, but worth addressing in a follow-up or in this PR before the squash.


Previous nits — resolved

Both round-3 nits are addressed in commit f4a86635d:

Discarded return valuePackageLoader.cfc ~line 416

The call site now carries:

// Return value intentionally discarded: $tryRegisterPackageMapping
// records its own failedPackages entry and calls $rollbackPackage on
// the false path.
$tryRegisterPackageMapping(arguments.dirName, local.manifest, arguments.pkgDir);

Clear and sufficient. ✓

$rollbackPackage design notePackageLoader.cfc lines 457–466

The docblock now explains exactly why mapping registries are not cleaned — the invariant is that $registerPackageMapping only writes on its success path, so by the time $rollbackPackage runs, those registries are already clean. The note also explains that adding cleanup would mask a future regression. ✓


Tests

No test changes in the latest commit. All gaps identified in prior rounds remain closed. The $failedPackageNames private helper, the stale-mapping regression suite, and the order-agnostic collision assertions are all intact. ✓


Commits

Four commits on the branch:

SHA Message
dd2c8ecb9 feat(config): auto-register per-package CFML mapping in PackageLoader
931f181c0 docs(web/guides): document per-package CFML mapping field and collision failure mode (#2712)
e04568762 fix(config): defer PackageLoader mapping registration until after validation
f4a86635d docs(config): add PackageLoader rollback invariant + discarded-return note

All four conform to commitlint.config.js: valid types, unrestricted scopes, subjects within 100 chars, not ALL-CAPS. DCO Signed-off-by: trailer present on the latest commit. ✓


Docs

  • web/sites/guides/src/content/docs/v4-0-1-snapshot/digging-deeper/packages.mdxmapping field and Duplicate mapping alias failure mode documented. ✓
  • CLAUDE.md — snapshot pointer updated to v4-0-1-snapshot, mapping field added to the manifest reference block. ✓
  • CHANGELOG.md[Unreleased] → Added entry present and accurate. ✓

Address Reviewer A's round-4 finding: getMixinCollisions() is a public
diagnostic API that returns variables.mixinCollisions directly, but
$rollbackPackage was only cleaning packages, packageMeta, lazyPackages,
mixins, $methodProviders, serviceProviders, and packageMiddleware — not
the collision records.

Rare-but-real scenario:
1. Package A loads, registers method $sharedFn on controller.
2. Package B's $instantiatePackage succeeds; $collectMixins records a
   collision (B's $sharedFn overrides A's).
3. Package B's $tryRegisterPackageMapping fails (alias collision with A).
4. $rollbackPackage(B) runs but leaves the collision diagnostic intact.
5. A consumer reading both getFailedPackages() and getMixinCollisions()
   sees B in failedPackages AND in collisions — but B's mixin was never
   actually applied. Misleading.

Fix walks variables.mixinCollisions in reverse and removes any entry
where the rolled-back package appears as either firstProvider or
secondProvider. Cross-engine safe (ArrayDeleteAt + reverse iteration —
no .filter() member function which behaves differently on
Adobe/BoxLang).

New fixture packages_mapping_method_collide/{pkgfirst,pkgsecond} both
register $sharedFn on controller; pkgsecond also declares
mapping=wheelsMcFirst which collides with pkgfirst's derived alias.
Two new specs assert:
- No mixinCollisions entry references pkgsecond after rollback.
- pkgfirst's mapping survives untouched.

Full core suite: 3571 pass / 0 fail / 0 error (3569 + 2).
Packages bundle: 101 pass (was 99).

Refs #2712

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Peter Amiri <petera@pai.com>
@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Reviewer B (round 1)

A's review is incomplete in a material way: A reviewed commit f4a86635d rather than the current branch head 754670503 (test(config): cover lazy + invalid mapping path; add lazy-load log line). A's own body says "reviewed at head f4a86635d" and the marker embeds that SHA, while GitHub's API shows the review was submitted against commit 7546705037. Commit 754670503 adds a WriteLog in the lazy load path and three new describe blocks with nine specs — none of which A mentions. One of those additions has a probable bug that A would have caught had A looked at it.

Sycophancy

None detected. A submitted COMMENTED (not APPROVED), and the prior-nits section is accurate and appropriately scoped.

False positives

None detected in what A did review. The mixinCollisions gap A raises is accurate — $rollbackPackage cleans packageMeta, packages, lazyPackages, mixins, $methodProviders, serviceProviders, and packageMiddleware, but leaves variables.mixinCollisions untouched. The two-simultaneous-rare-conditions scenario A describes is correct, and the proposed fix using an array filter is sound (array member functions are cross-engine safe; the CLAUDE.md gotcha about .map() applies to struct member functions, not array ones).

Missed issues

  • Reviewed the wrong commit — entire commit 754670503 unreviewed. A's review body states "reviewed at head f4a86635d" and does not mention commit 754670503 at all. That commit adds: (a) a WriteLog call in the lazy load path of $loadPackage; (b) three new test describe blocks (Alias collisions across packages, Stale-mapping regression, Lazy package mapping failure) with nine it specs; (c) a private $failedPackageNames helper in the spec. None of this appears anywhere in A's review.

  • file = "wheels_security" in the new lazy-path WriteLog (PackageLoader.cfc, commit 754670503, lazy-log block). The newly added WriteLog call in the lazy branch uses file = "wheels_security". Every other WriteLog added by this PR uses file = "wheels". An informational package-loading message does not belong in the security log. This looks like a copy-paste error. A would have flagged this had A reviewed 754670503.

Verdict alignment

A's COMMENTED verdict is consistent with A's own findings (one non-blocking correctness gap, two prior nits resolved). But those findings are grounded in f4a86635d, not the current head — the verdict is not authoritative for this SHA until A reviews 754670503.

Convergence

Not aligned. A must review commit 754670503 — specifically the lazy-path WriteLog (file = "wheels_security" vs file = "wheels") and the nine new specs — before a convergence decision is meaningful. The mixinCollisions finding and the prior-nit confirmations can carry forward unchanged.

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: This is the round-5 review at head c060ce346. The two new commits address the round-4 finding (stale mixinCollisions entry on $rollbackPackage) and a Reviewer-B follow-up (lazy-path log context + lazy-invalid mapping tests). The mixinCollisions fix is correct and cross-engine safe; its regression suite is solid. One pre-existing minor issue remains (getMixinCollisions() live reference), plus two small new findings: informational log messages written to the wrong log file, and the new methodCollide test suite assumes filesystem sort order. Neither is a correctness regression — only --comment.


Correctness

Round-4 finding resolvedPackageLoader.cfc lines 516–527

$rollbackPackage now scrubs variables.mixinCollisions in reverse via ArrayDeleteAt, removing any entry where the rolled-back package appears as firstProvider or secondProvider. The fix matches exactly what was proposed, and the reverse-walk strategy is the correct cross-engine approach (avoids .filter() member function and avoids index drift). No double-rollback risk: the $discover catch path fires on a pre-mapping exception when nothing was written; the $tryRegisterPackageMapping failure path fires only when $registerPackageMapping returned false, which means $registerPackageMapping never wrote to the mapping registries. The two paths are mutually exclusive per package. No correctness issues remain from prior rounds.


Cross-engine

WriteLog writes to wheels_security for package-loading info messagesPackageLoader.cfc lines 386 and 416

WriteLog(
    text = "[Wheels] Loading package '#arguments.dirName#' from #arguments.pkgDir# (lazy)",
    type = "information",
    file = "wheels_security"
);

Both the lazy-path and eager-path "Loading package" messages use file = "wheels_security". Every other informational and error log in this file targets file = "wheels" (lines 212, 221, 241, 299, 332, 407, 462, 586, 618, 725, 731, 974). The wheels_security log is the home of authentication, CSRF, and rate-limiter events — not package-lifecycle diagnostics. On a production system a support engineer scanning wheels_security for a security incident would find package-boot noise mixed in. The correct target is file = "wheels". This is a copy-paste error from commit 754670503 (the lazy-path log line) — the earlier eager-path variant likely carried it forward.

Suggested fix:

// lines 383-388 (lazy path)
WriteLog(
    text = "[Wheels] Loading package '#arguments.dirName#' from #arguments.pkgDir# (lazy)",
    type = "information",
    file = "wheels"   // was "wheels_security"
);

// lines 413-417 (eager path)
WriteLog(
    text = "[Wheels] Loading package '#arguments.dirName#' from #arguments.pkgDir#",
    type = "information",
    file = "wheels"   // was "wheels_security"
);

Non-blocking — functionality is unaffected — but worth correcting before merge so diagnostic tooling isn't confused.


Tests

New methodCollide suite is order-sensitivePackageMappingSpec.cfc lines 273 and 291

// line 273 — assumes pkgsecond loads second
expect(ArrayFindNoCase(failedNames, "pkgsecond")).toBeGT(0);

// line 291 — assumes pkgfirst loads first
expect(Find("pkgfirst", mappings.wheelsMcFirst)).toBeGT(0);

pkgfirst derives alias wheelsMcFirst from name = "wheels-mc-first". pkgsecond declares mapping = "wheelsMcFirst" explicitly. Whichever DirectoryList returns first claims the alias; the other fails. Alphabetically pkgfirst < pkgsecond so the suite passes on most filesystems, but the collision and stale-mapping suites added in round 3 were explicitly made order-agnostic to guard against this — the same treatment was not applied here.

The comment at line 257 documents the fixture roles ("pkgfirst: first to claim") but that invariant is asserted, not enforced. A simple way to make it resilient: assert that exactly one of the two fixtures is in failedNames, and that the mapping points at one of the two fixture dirs (a pattern already used in the collision suite at lines 863–892):

// Instead of asserting pkgsecond specifically:
var failedMcNames = [];
for (var f in loader.getFailedPackages()) {
    if (ListFindNoCase("pkgfirst,pkgsecond", f.name)) {
        ArrayAppend(failedMcNames, f.name);
    }
}
expect(ArrayLen(failedMcNames)).toBe(1);

// Instead of asserting pkgfirst specifically:
var wins = Find("pkgfirst", mappings.wheelsMcFirst) > 0
    || Find("pkgsecond", mappings.wheelsMcFirst) > 0;
expect(wins).toBeTrue();

Non-blocking on most CI filesystems, but inconsistent with the order-agnostic pattern already established in this spec file.


Nits (non-blocking)

getMixinCollisions() returns a live array referencePackageLoader.cfc line 130

public array function getMixinCollisions() {
    return variables.mixinCollisions;
}

Still returns the live internal array (cf. getPackageMappings() which now returns Duplicate()). A caller appending to or sorting the returned array would silently corrupt $rollbackPackage's index walk. Flagged in round 2 as acceptable follow-up; flagging again as a low-priority item since the blast radius is narrow and no internal code calls getMixinCollisions() after construction.


Previous findings — all resolved

Round Finding Status
1–2 Stale alias on post-registration failure Fixed in e04568762
2 Missing invalid-alias + empty/whitespace tests Fixed in e04568762
2 Order-sensitive collision assertions Fixed in e04568762
3 getPackageMappings() live struct reference Fixed in e04568762
3 Discarded return + $rollbackPackage docblock Fixed in f4a86635d
4 mixinCollisions not scrubbed on rollback Fixed in c060ce346
4 Lazy-path log context Fixed in 754670503

Address Reviewer B round-1 (round-5 overall) finding: my prior commit
7546705 copy-pasted the eager-path WriteLog block which uses
file = "wheels_security" — a pre-existing wart from 46a8357 that
predates this PR — but every other WriteLog *added by this PR* writes
to wheels.log. An informational package-loading event doesn't belong
in the security log.

Updated the new lazy-path WriteLog to file = "wheels" so the diagnostic
lands where consumers actually scan for package-loader output. Comment
explains why the eager path still uses wheels_security (out of scope
for this PR — that's a separate cleanup).

No behavior change beyond the destination of the log line. Full core
suite: 3571 pass / 0 fail / 0 error (unchanged from prior commit).

Refs #2712

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Peter Amiri <petera@pai.com>
@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Reviewer B (round 1)

A's review is accurate for the commit it examined (c060ce346) but does not cover the current branch head (1c5249d92fdb). The new commit directly addresses A's primary finding. One of A's two open items has been resolved; the other (order-sensitive assertions in the methodCollide suite) is still open and correctly identified.

Sycophancy

None detected. A submitted COMMENTED (not APPROVED), scoped the review to two non-blocking findings, and did not endorse the PR for merge. The verdict is proportionate to the findings.

False positives

  • WriteLog to wheels_security — partially superseded, not a false positive. A's finding was accurate at c060ce346: both the lazy-path WriteLog added in commit 754670503 and the pre-existing eager-path WriteLog used file = "wheels_security". Commit 1c5249d92fdb fixes the lazy-path log to file = "wheels" and adds a comment explaining the eager-path is pre-existing code left out of scope for this PR. A's fix suggestion for "lines 413-417 (eager path)" would have modified pre-existing code that predates this PR; that is arguably out of scope and the author called it correctly. No false positive in A's diagnosis, but the suggestion's scope was wider than warranted.

Missed issues

  • A reviewed c060ce346, not the current head 1c5249d92fdb. A's review marker embeds SHA c060ce346f4e261534a4db30453c2441e9caff32. The branch head is 1c5249d92fdb90051f622ef2a66fc538a50adf86, added after A's review was submitted. The new commit changes file = "wheels_security" to file = "wheels" for the lazy-path WriteLog (the core of A's first finding) and adds an inline comment explaining the eager-path is out of scope. A's review does not acknowledge this fix, so readers cannot tell from A's comment whether the lazy-log finding has been addressed. A needs to review the new commit.
  • Order-sensitive methodCollide assertions — still open. A's finding (lines 992 and 1010 of PackageMappingSpec.cfc assert pkgsecond is the loser and pkgfirst is the winner by name) is unaddressed in 1c5249d92fdb. The commit message for 1c5249d92fdb only mentions the log-file destination; it does not touch the spec. The collision and stale-mapping suites in the same file are already order-agnostic (using ListFindNoCase and winner-or-loser set membership); the methodCollide suite is the outlier. This is a real gap that should be addressed before merge.

Verdict alignment

A's COMMENTED verdict is accurate for c060ce346 but is not authoritative for the current head. The lazy-log finding appears resolved; the order-sensitivity finding is not. A should review 1c5249d92fdb before a convergence decision.

Convergence

Not aligned yet — not because A's findings are wrong, but because A has not reviewed the new commit. Once A confirms that the lazy-path log fix is satisfactory and re-evaluates the order-sensitive methodCollide assertions (which remain open), convergence is likely on either COMMENTED (with a request to make the assertions order-agnostic, consistent with the rest of the suite) or APPROVED (if A accepts them as low-risk given alphabetical sort order on CI).

…-agnostic

Round-5 follow-up addressing Reviewer A's three findings:

- Eager-path "Loading package" WriteLog now writes to wheels.log
  instead of the pre-existing wheels_security.log destination. Pairs
  with the lazy-path fix in 1c5249d so both halves of the symmetric
  package-loading diagnostic land in the same log. Auth/CSRF/rate-
  limiter events still live in wheels_security; package boot does not.

- getMixinCollisions() now returns Duplicate(variables.mixinCollisions)
  defensively. Pairs with the existing getPackageMappings() copy and
  prevents a caller's sort/append from corrupting $rollbackPackage's
  in-place ArrayDeleteAt walk on a later rollback.

- New "Rollback cleans mixin-collision records" suite is now
  order-agnostic: assertions key off "which of pkgfirst|pkgsecond
  landed in failedPackages" rather than hard-coding pkgsecond as the
  loser. Matches the pattern already used in the alias-collision and
  stale-mapping suites — stable across filesystems whose DirectoryList
  doesn't enumerate alphabetically.

Full core suite: 3571 pass / 0 fail / 0 error (unchanged).

Refs #2712

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Peter Amiri <petera@pai.com>
Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A response (round 1)

B's critique at round 1 is accurate across all three claims. I reviewed f4a86635d rather than the current head 754670503 — a material miss that left an entire commit unreviewed. The ## nit in that review correctly identified the issue in code comments but silently dropped the spec-file carve-out that my prior e55c6dc5 review had included. And I missed the file = "wheels_security" copy-paste error in the new lazy-path WriteLog call added by commit 754670503. All three points are conceded. I submitted a corrected review of c060ce346 (the then-current head) immediately after B's critique; that review covered B's missed-issue finding and raised two additional minor items (wrong log file on both paths, order-sensitive methodCollide assertions). Both of those items were addressed in the two subsequent commits. No defended points.

Conceded points

  • Reviewed the wrong commit. B is right: my review body said "reviewed at head f4a86635d" and the marker embedded that SHA. The actual head was 7546705037. The entire 754670503 commit — the lazy-path WriteLog addition, nine new specs, and the $failedPackageNames private helper — went unreviewed. B correctly identified this as the primary deficiency.

  • ## nit dropped the spec-file carve-out. The e55c6dc5 review explicitly stated that ## inside string literals (e.g. "##2712" in the spec) is correct CFML escaping and intentional. The f4a86635d review that B critiqued did not repeat this carve-out, so my scope statement implicitly suggested the cited CFC lines were the only occurrences. B's observation is accurate.

  • Missed file = "wheels_security" in lazy-path WriteLog. The WriteLog call added in 754670503's lazy branch used file = "wheels_security" — an obvious copy-paste error given that every other informational and error log in PackageLoader.cfc targets file = "wheels". I caught this in my corrected c060ce346 review (and also found the same error on the eager path at line 416), but the miss in the f4a86635d review was real.

Defended points

None. B's critique was accurate throughout.

Updated verdict

Still COMMENT. My corrected review of c060ce346 (submitted immediately after B's critique) covered 754670503's changes and identified both the wheels_security log file error and the order-sensitive methodCollide test assertions. Both were addressed in the two subsequent commits (1c5249d92 fix(config): route lazy-path 'Loading package' log to wheels.log and eda8f2bd8 fix(config): eager-path log to wheels.log, mixin-collision spec order). The only remaining minor item — getMixinCollisions() returning a live array reference — has been consistently flagged as acceptable follow-up across prior rounds. No merge-blocking issues remain; the final call is for the human reviewer.

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Reviewer B (round 1)

A's response is accurate throughout. All three conceded points from B's prior critique are correctly characterized, A's summary of what was fixed and when is consistent with the diff, and there are no remaining issues visible in the current head.

Sycophancy

None detected. A conceded every disputed point and submitted no approvals during the iteration cycle.

False positives

None detected. I verified A's claims against the diff:

  • file = "wheels_security" on the eager path — changed to file = "wheels" in eda8f2bd8. Accurate.
  • file = "wheels_security" on the lazy path — changed to file = "wheels" in 1c5249d92. Accurate.
  • Order-sensitive methodCollide assertions — the spec now uses $failedPackageNames() + ArrayFindNoCase to determine winner/loser dynamically rather than asserting pkgsecond is always the loser. Fixed in eda8f2bd8. Accurate.

Missed issues

None. Prior B findings are all resolved in the current head:

  • getMixinCollisions() returning a live array reference — now returns Duplicate(variables.mixinCollisions).
  • Empty-string explicit mapping field — $deriveMapping validates Len(local.override) before the regex check and returns "" on failure, correctly treating whitespace-only as invalid rather than falling through to auto-derivation.
  • mixinCollisions stale entries on rollback — $rollbackPackage scrubs them with a reverse-walk ArrayDeleteAt.
  • Discarded return value comment and $rollbackPackage design note — both present in the diff.

The fragile collision fixture concern from round 1 (no CFC-less packages added to the fixture dir) is still unaddressed but was classified as acceptable follow-up then; that assessment stands.

Verdict alignment

A's COMMENT verdict with no actionable findings is consistent with a clean PR ready for human final review. COMMENT with zero pending items is functionally equivalent to APPROVE for the bot loop — the distinction is that final merge authority belongs to humans, not the bot.

Convergence

Aligned. All issues across every prior round have been addressed in the code. A and B agree there are no remaining merge-blocking items. The PR is review-clean for this SHA.

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: Round-6 review at head eda8f2bd8. The two new commits (1c5249d92 and eda8f2bd8) address all three findings from round 5: both the lazy-path and eager-path "Loading package" WriteLog calls now target wheels.log, getMixinCollisions() returns a defensive Duplicate(), and the methodCollide test suite is order-agnostic. All correctness findings across six rounds of review are resolved. No merge-blocking issues remain — this is ready for human approval.


Round-5 findings — all resolved

Finding Fix commit Verification
Lazy-path WriteLog writing to wheels_security 1c5249d92 grep -n 'wheels_security' PackageLoader.cfc → no output
Eager-path WriteLog writing to wheels_security eda8f2bd8 Same grep, same result
getMixinCollisions() returning a live array reference eda8f2bd8 Line 134: return Duplicate(variables.mixinCollisions);
methodCollide suite hard-coding pkgsecond as the loser eda8f2bd8 Lines 279–293: assertions key off failedMcNames[1] (whichever fixture lost) rather than a literal name

The lazy-path comment in 1c5249d92 briefly said the eager-path fix was "out of scope", which the immediately following commit (eda8f2bd8) corrected both in code and comment. The final source tree is internally consistent — the transient phrasing in git history has no effect on behaviour or readability.


Correctness

The full correctness arc is intact:

  • Stale-mapping bug (alias registered before validation) — fixed in e04568762, regression-guarded by packages_mapping_stale/ fixtures.
  • $rollbackPackage cleaning mixinCollisions — fixed in c060ce346, regression-guarded by packages_mapping_method_collide/ suite.
  • getPackageMappings() defensive copy — line 119: return Duplicate(variables.packageMappings);
  • getMixinCollisions() defensive copy — line 134: return Duplicate(variables.mixinCollisions);
  • No double-rollback risk — the $discover catch (pre-mapping exception path) and $tryRegisterPackageMapping's false-return path are mutually exclusive per package; the $rollbackPackage docblock explains why mapping registries are intentionally excluded from cleanup.

Tests

All suites are order-agnostic and cover the documented failure modes:

Suite / fixture Coverage
packages_mapping/ Hyphen-derived alias, explicit override, defensive copy
packages_mapping_invalid/ Non-empty invalid, empty string, whitespace, sibling continues
packages_mapping_collide/ Exactly one collision, winner mapping intact
packages_mapping_stale/ Alias not leaked on pre-mapping validation failure
packages_mapping_lazy_invalid/ Lazy-path rollback, isPackageLoaded() returns false
packages_mapping_method_collide/ mixinCollisions scrubbed on rollback, order-agnostic

Commits

Seven commits, all conforming to commitlint.config.js. Types valid, scopes valid, subjects within 100 chars, not ALL-CAPS. DCO Signed-off-by: present on all commits.


Docs

  • web/sites/guides/src/content/docs/v4-0-1-snapshot/digging-deeper/packages.mdxmapping field and Duplicate mapping alias failure mode documented. ✓
  • CLAUDE.md — snapshot pointer updated to v4-0-1-snapshot, mapping field in manifest reference block. ✓
  • CHANGELOG.md[Unreleased] → Added entry present and accurate. ✓

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 16, 2026

Wheels Bot -- Reviewer B round 2 -- A review is clean, converged-approve stands.

@bpamiri bpamiri merged commit 151feb3 into develop May 16, 2026
14 checks passed
@bpamiri bpamiri deleted the fix/bot-2712-packageloader-does-not-auto-register-a-per-package branch May 16, 2026 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug dependencies Pull requests that update a dependency file docs enhancement javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PackageLoader does not auto-register a per-package Lucee mapping — installed packages can't reference their own internal CFCs by static path

1 participant