fix: resolve Pro package from both @aiox-fullstack/pro and @aios-fullstack/pro [Story 122.1]#623
fix: resolve Pro package from both @aiox-fullstack/pro and @aios-fullstack/pro [Story 122.1]#623rafaelscosta wants to merge 7 commits intomainfrom
Conversation
…stack/pro [Story 122.1] - pro-detector: add npm package resolution with canonical/fallback priority - pro CLI commands: resolve license path from both scopes - pro-setup wizard: loadProModule tries both npm scopes - pro-scaffolder: resolve source dir from both npm scopes - aiox-pro-cli: isProInstalled checks both scopes - i18n: update user-facing messages to active package name - CI: update publish workflow to active package name - tests: update pro-detector tests for new API (21/21 passing) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds dual-scope Pro package resolution across CLI, installer, and detection utilities; introduces a new Pro updater module and Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI (aiox)
participant Updater as Pro Updater
participant Registry as npm Registry (HTTPS)
participant PM as Package Manager
participant FS as Filesystem / Scaffolder
CLI->>Updater: updatePro(projectRoot, options)
Updater->>Updater: resolveInstalledPro(projectRoot)
Updater->>Registry: fetchLatestFromNpm(packageName)
Registry-->>Updater: metadata (version, peerDependencies)
Updater->>Updater: compare versions & peer deps
alt includeCore & core incompatible
Updater->>PM: install/update aiox-core
PM-->>Updater: install result
end
alt should update Pro
Updater->>PM: install/update Pro (or dry-run plan)
PM-->>Updater: install result
Updater->>FS: require scaffolder -> scaffoldProContent(...)
FS-->>Updater: scaffold result
else up-to-date
Updater-->>CLI: formatted up-to-date result
end
Updater-->>CLI: formatUpdateResult(result)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📊 Coverage ReportCoverage report not available
Generated by PR Automation (Story 6.1) |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/installer/src/wizard/i18n.js (1)
124-165:⚠️ Potential issue | 🟡 MinorKeep these recovery strings scope-agnostic.
The wizard now resolves
@aiox-fullstack/profirst, but these messages tell users to install only@aios-fullstack/pro. Once the canonical package is available, valid installs will be reported with the wrong remediation path. Prefer “AIOX Pro package” or mention both scopes here.Also applies to: 282-322, 439-479
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/installer/src/wizard/i18n.js` around lines 124 - 165, The i18n strings reference the hard-coded scope '@aios-fullstack/pro' which is now ambiguous because the wizard resolves '@aiox-fullstack/pro' first; update all affected keys (e.g., proModuleNotAvailable, proInstallingPackage, proPackageInstallFailed, proPackageNotFound, proScaffolderNotAvailable, proNeedHelp and the other messages in the ranges mentioned) to use scope-agnostic wording such as "AIOX Pro package" or list both scopes ('@aiox-fullstack/pro' and '@aios-fullstack/pro') in remediation text, ensuring every message that tells users to install or check the package is updated consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.aiox-core/cli/commands/pro/index.js:
- Around line 636-659: The loop currently falls back to the next package for any
execSync error; change the catch in the for (const pkg of PRO_PACKAGES) loop to
inspect the thrown error (e.g., capture as err) and only continue to the next
pkg when the error indicates package-not-found (look for stderr/message
containing "404", "Not Found", "ERR! 404" or similar). For any other error
(auth/registry/lockfile/etc.) surface it immediately: log the error details via
console.error and exit (or rethrow) instead of silently trying the fallback;
keep references to PRO_PACKAGES and the execSync call while updating the catch
behavior.
In `@bin/utils/pro-detector.js`:
- Around line 49-63: The resolveNpmProPackage function currently checks only
hardcoded node_modules locations and fails for hoisted/workspace installs;
modify resolveNpmProPackage to attempt resolving each candidate using
require.resolve (e.g., require.resolve(path.join(candidateName, 'package.json'))
or require.resolve(candidateName) inside a try/catch) so Node's resolver is
used, and on success return the same { packagePath, packageName } shape (use
path.dirname of the resolved package.json or module to set packagePath); keep
trying PRO_PACKAGE_CANONICAL then PRO_PACKAGE_FALLBACK and return null if both
fail.
In `@packages/aiox-pro-cli/bin/aiox-pro.js`:
- Around line 25-26: The rename introduced PRO_PACKAGE_CANONICAL and
PRO_PACKAGE_FALLBACK but left references to PRO_PACKAGE in showHelp(),
installPro(), and the “not installed” path, causing a ReferenceError; fix by
restoring a compatibility alias (e.g., define PRO_PACKAGE =
PRO_PACKAGE_CANONICAL) or update every call site to use
PRO_PACKAGE_CANONICAL/PRO_PACKAGE_FALLBACK consistently (ensure showHelp,
installPro, and the not-installed logic reference the new constants).
In `@packages/installer/src/wizard/pro-setup.js`:
- Around line 340-346: The loops over npmScopes currently swallow all require()
errors; change the catch to only fall through when the module is actually
missing for the exact requested path. For each loop (the one using npmScopes and
moduleName and the other similar loop referenced), compute the request string
(e.g. const reqPath = `${scope}/license/${moduleName}`), try require(reqPath),
and in the catch inspect the error: if err.code !== 'MODULE_NOT_FOUND' or the
err.message does not include reqPath then rethrow the error; otherwise continue
to the next scope. This ensures you only fallback on a genuine "module not
found" for that specific path and rethrow initialization or transitive
dependency errors.
In `@tests/pro/pro-detector.test.js`:
- Around line 68-73: Add a targeted test that calls resolveNpmProPackage()
directly with both candidate scopes present and asserts the returned package
string is the canonical '@aiox-fullstack/pro' (i.e., ensure the resolver prefers
'@aiox-fullstack' over the fallback); to implement, mock fs.existsSync to return
true for paths containing both '@aiox-fullstack' and '@aios-fullstack' and then
assert resolveNpmProPackage() === '@aiox-fullstack/pro'; replicate the same
explicit precedence assertion for the similar case mentioned around lines 90-94
so the new resolver ordering is covered.
---
Outside diff comments:
In `@packages/installer/src/wizard/i18n.js`:
- Around line 124-165: The i18n strings reference the hard-coded scope
'@aios-fullstack/pro' which is now ambiguous because the wizard resolves
'@aiox-fullstack/pro' first; update all affected keys (e.g.,
proModuleNotAvailable, proInstallingPackage, proPackageInstallFailed,
proPackageNotFound, proScaffolderNotAvailable, proNeedHelp and the other
messages in the ranges mentioned) to use scope-agnostic wording such as "AIOX
Pro package" or list both scopes ('@aiox-fullstack/pro' and
'@aios-fullstack/pro') in remediation text, ensuring every message that tells
users to install or check the package is updated consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1ed22e78-c0c2-4bd4-963e-abc24839acf8
📒 Files selected for processing (8)
.aiox-core/cli/commands/pro/index.js.github/workflows/publish-pro.ymlbin/utils/pro-detector.jspackages/aiox-pro-cli/bin/aiox-pro.jspackages/installer/src/pro/pro-scaffolder.jspackages/installer/src/wizard/i18n.jspackages/installer/src/wizard/pro-setup.jstests/pro/pro-detector.test.js
- Add pro-updater.js: detect installed Pro, query npm, check compat, update, re-scaffold - Register 'aiox pro update' subcommand with --check, --dry-run, --force, --include-core, --skip-scaffold - Add 'aiox update --include-pro' to update core + Pro in one command - License validation before update (skip on --check/--dry-run) - Detects package manager (npm/pnpm/yarn/bun) automatically - Re-scaffolds Pro assets (squads, skills, configs) after package update - Formatted CLI output with update summary Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
.aiox-core/cli/commands/pro/index.js (1)
643-652:⚠️ Potential issue | 🟠 MajorRetry the fallback only for “package not found”, and pin the intended fallback version.
This still falls through to
@aios-fullstack/proafter any install failure, so auth, registry, or lockfile errors get masked as a scope fallback. It also installs the fallback unpinned even though this story’s contract is@aios-fullstack/pro@0.3.0, which makes setup drift with whateverlatestpoints to at execution time. As per coding guidelines,**/*.js: Verify error handling is comprehensive.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.aiox-core/cli/commands/pro/index.js around lines 643 - 652, The current loop over PRO_PACKAGES blindly falls back on any install error and installs the unpinned fallback; change the catch in the loop that calls execSync to inspect the thrown error (from execSync) and only continue to the next package when the error indicates "package not found" (npm ERR 404 or similar message/code), otherwise rethrow or exit with the original error so auth/registry/lockfile errors are not masked; also update the PRO_PACKAGES entry for the fallback to use the pinned version "@aios-fullstack/pro@0.3.0" so the intended version is installed when that specific fallback is chosen (references: PRO_PACKAGES array, execSync call, and the catch block that sets installed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.aiox-core/cli/commands/pro/index.js:
- Around line 707-712: The command defines conflicting flags
(.option('--include-core') and .option('--no-core')) so --no-core doesn't negate
includeCore; update the CLI option definitions to use a single boolean flag with
a negation pair (e.g., define .option('--core') and .option('--no-core') or only
.option('--include-core') and remove .option('--no-core')) and ensure the
handler reads the same property passed to updatePro (currently reading
options.includeCore); update the option name or the code that builds the call to
updatePro so options.includeCore correctly reflects the user's --core/--no-core
choice and passes the intended includeCore value into updatePro.
In @.aiox-core/core/pro/pro-updater.js:
- Around line 301-309: The scaffold step currently ignores
scaffoldResult.success and unconditionally sets result.success = true; update
the logic in the block that calls runScaffold (referencing skipScaffold,
updatedPro/installed to compute proPath, runScaffold, and
result.scaffoldResult/result.actions) so that if scaffoldResult.success is false
you set result.success = false (and keep the action marked 'failed') and return
or propagate that failure; also wrap the runScaffold call in a try/catch so any
thrown errors are caught, set result.scaffoldResult to an error/failure object,
push a corresponding failed action into result.actions, and set result.success =
false before returning.
- Around line 119-129: The current satisfiesPeer function incorrectly strips
operators and compares numeric minima; replace its logic to use a real semver
evaluator: import or require the semver package and change
satisfiesPeer(installed, range) to return true when either arg is missing,
otherwise call semver.satisfies(installed, range) (or
semver.satisfies(semver.coerce(installed), range) if installed may be
non-standard), and catch invalid-range errors to conservatively return
true/false per your policy; ensure you update any module imports to include
semver so semver.satisfies is available.
---
Duplicate comments:
In @.aiox-core/cli/commands/pro/index.js:
- Around line 643-652: The current loop over PRO_PACKAGES blindly falls back on
any install error and installs the unpinned fallback; change the catch in the
loop that calls execSync to inspect the thrown error (from execSync) and only
continue to the next package when the error indicates "package not found" (npm
ERR 404 or similar message/code), otherwise rethrow or exit with the original
error so auth/registry/lockfile errors are not masked; also update the
PRO_PACKAGES entry for the fallback to use the pinned version
"@aios-fullstack/pro@0.3.0" so the intended version is installed when that
specific fallback is chosen (references: PRO_PACKAGES array, execSync call, and
the catch block that sets installed).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dcfbf71d-9874-4c1a-ae37-69cc8d989f90
📒 Files selected for processing (3)
.aiox-core/cli/commands/pro/index.js.aiox-core/core/pro/pro-updater.jsbin/aiox.js
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
.aiox-core/core/pro/pro-updater.js (2)
30-35: Consider reusingdetectPackageManagerfromdependency-installer.js.The same logic exists in
packages/installer/src/installer/dependency-installer.jswith a data-driven approach usingLOCK_FILES. Extracting this to a shared utility would reduce duplication and ensure consistent priority order across the codebase.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.aiox-core/core/pro/pro-updater.js around lines 30 - 35, The detectPackageManager implementation in pro-updater.js duplicates logic from dependency-installer.js; extract the lock-file based detection into a shared utility (e.g., create a new exported function like detectPackageManager in a common util module) that uses the existing LOCK_FILES data-driven array from dependency-installer.js (or move LOCK_FILES into the new shared module) and replace the local detectPackageManager function with an import of that shared function so both pro-updater.js and dependency-installer.js use the same implementation and priority order.
215-223: Consider validatingprojectRootparameter.Public API methods like
updatePro(),getCoreVersion(), andresolveInstalledPro()don't validate thatprojectRootis a non-empty string or an existing directory. Passingundefinedor an invalid path would cause crypticfserrors downstream. As per coding guidelines,.aiox-core/core/**: Check for proper input validation on public API methods.💡 Example validation
async function updatePro(projectRoot, options = {}) { if (typeof projectRoot !== 'string' || !projectRoot) { throw new TypeError('projectRoot must be a non-empty string'); } // ... rest of function }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.aiox-core/core/pro/pro-updater.js around lines 215 - 223, Validate the projectRoot parameter at the start of public API functions (updatePro, getCoreVersion, resolveInstalledPro): ensure typeof projectRoot === 'string' and it is not empty, then confirm it points to an existing directory (use fs.stat or fs.promises.stat and check isDirectory()); throw a clear TypeError for invalid types/empty string and a descriptive Error if the path does not exist or is not a directory so downstream fs calls won't produce cryptic errors.tests/pro/pro-updater.test.js (1)
119-138:jest.doMockwithoutjest.resetModules()may cause flaky tests.When using
jest.doMockfor lazy mocking after the module under test is already imported, you should calljest.resetModules()beforehand to clear the require cache. Otherwise, if the scaffolder module was required elsewhere (e.g., in a previous test or during import resolution), the mock won't take effect.♻️ Suggested fix
+ jest.resetModules(); jest.doMock(scaffolderPath, () => ({ scaffoldProContent: jest.fn().mockResolvedValue({ success: false, errors: ['sync failed'], copiedFiles: [], skippedFiles: [], warnings: [], }), })); + + // Re-require updater to pick up mocked scaffolder + const { updatePro: updateProWithMock } = require('../../.aiox-core/core/pro/pro-updater'); - const result = await updatePro(projectRoot, {}); + const result = await updateProWithMock(projectRoot, {});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/pro/pro-updater.test.js` around lines 119 - 138, The test uses jest.doMock(scaffolderPath, ...) after the module under test has likely been imported, which can lead to flaky behavior; call jest.resetModules() immediately before jest.doMock to clear the require cache so the mock will be applied, then re-require or import modules if needed; update the test around the jest.doMock call that mocks scaffolderPath (and the subsequent updatePro invocation) to add jest.resetModules() before doMock and ensure jest.dontMock(scaffolderPath) remains after the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.aiox-core/core/pro/pro-updater.js:
- Around line 48-62: The response handler currently parses body unconditionally;
update the res.on('end') logic to first check res.statusCode (ensure 2xx, e.g.,
statusCode >= 200 && statusCode < 300) before attempting JSON.parse. If the
status is not OK, do not parse the body—resolve(null) (or reject with a clear
error) and include the statusCode in any logged message; keep the existing
try/catch JSON.parse block only for successful status codes. Target the
res.on('end') callback (the JSON.parse block that builds { version,
peerDependencies }) and the surrounding https.get response handler to implement
this check.
- Around line 391-404: The current brittle relative import built via
scaffolderPath (path.join(...
'packages','installer','src','pro','pro-scaffolder')) and its require that
extracts scaffoldProContent should be replaced with a stable package export or
module alias: add an "exports" entry in packages/installer/package.json that
exposes "src/pro/pro-scaffolder" (or create a tsconfig/webpack module alias for
the installer package), then change the require in pro-updater.js to import from
the package name (e.g., require('installer/pro-scaffolder') or the alias) and
keep using the scaffoldProContent symbol; update any build/tsconfig config so
the new path resolves in dev and CI.
---
Nitpick comments:
In @.aiox-core/core/pro/pro-updater.js:
- Around line 30-35: The detectPackageManager implementation in pro-updater.js
duplicates logic from dependency-installer.js; extract the lock-file based
detection into a shared utility (e.g., create a new exported function like
detectPackageManager in a common util module) that uses the existing LOCK_FILES
data-driven array from dependency-installer.js (or move LOCK_FILES into the new
shared module) and replace the local detectPackageManager function with an
import of that shared function so both pro-updater.js and
dependency-installer.js use the same implementation and priority order.
- Around line 215-223: Validate the projectRoot parameter at the start of public
API functions (updatePro, getCoreVersion, resolveInstalledPro): ensure typeof
projectRoot === 'string' and it is not empty, then confirm it points to an
existing directory (use fs.stat or fs.promises.stat and check isDirectory());
throw a clear TypeError for invalid types/empty string and a descriptive Error
if the path does not exist or is not a directory so downstream fs calls won't
produce cryptic errors.
In `@tests/pro/pro-updater.test.js`:
- Around line 119-138: The test uses jest.doMock(scaffolderPath, ...) after the
module under test has likely been imported, which can lead to flaky behavior;
call jest.resetModules() immediately before jest.doMock to clear the require
cache so the mock will be applied, then re-require or import modules if needed;
update the test around the jest.doMock call that mocks scaffolderPath (and the
subsequent updatePro invocation) to add jest.resetModules() before doMock and
ensure jest.dontMock(scaffolderPath) remains after the assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7aff28b9-5d29-4de0-b513-ff982f4a8df6
📒 Files selected for processing (7)
.aiox-core/cli/commands/pro/index.js.aiox-core/core/pro/pro-updater.js.aiox-core/install-manifest.yamlbin/utils/pro-detector.jspackages/installer/src/wizard/pro-setup.jstests/pro/pro-detector.test.jstests/pro/pro-updater.test.js
✅ Files skipped from review due to trivial changes (2)
- .aiox-core/install-manifest.yaml
- bin/utils/pro-detector.js
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/installer/src/wizard/pro-setup.js
- tests/pro/pro-detector.test.js
- .aiox-core/cli/commands/pro/index.js
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.aiox-core/core/pro/pro-updater.js (1)
233-242: Consider adding input validation forprojectRoot.The
updateProfunction is a public API but doesn't validate thatprojectRootis a valid, existing directory before proceeding. If called with an invalid path, the failure would occur deep in the flow with a potentially confusing error message.🛡️ Suggested validation
async function updatePro(projectRoot, options = {}) { + if (!projectRoot || typeof projectRoot !== 'string') { + return { + success: false, + error: 'projectRoot is required and must be a string.', + actions: [], + }; + } + + if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) { + return { + success: false, + error: `Project root does not exist or is not a directory: ${projectRoot}`, + actions: [], + }; + } + const { check = false,As per coding guidelines,
.aiox-core/core/**: Check for proper input validation on public API methods.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.aiox-core/core/pro/pro-updater.js around lines 233 - 242, The updatePro(public API) is missing validation for its projectRoot parameter; add an early guard in the updatePro function to ensure projectRoot is a non-empty string that resolves to an existing directory and throw a clear, descriptive error (e.g., TypeError or custom Error) if not. Use the Node fs APIs (fs.stat or fs.promises.stat) to test existence and directory-ness before any async work begins, normalize the path (e.g., path.resolve) for clearer error messages, and reference updatePro and the projectRoot parameter in the thrown message so callers immediately know the input was invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.aiox-core/core/pro/pro-updater.js:
- Around line 348-361: The update block hardcodes 'aiox-core' when calling
buildInstallCmd(pm, 'aiox-core') which can install the wrong package for
projects using the scoped name; add a helper (e.g.,
detectCorePackageName(projectRoot)) that reads package.json and dependencies to
return '@synkra/aiox-core' or 'aiox-core' (or null), call it before the
includeCoreUpdate branch, and use its result as the packageName argument to
buildInstallCmd (falling back to 'aiox-core' if detectCorePackageName returns
null); update the try/catch block in the includeCoreUpdate section to use this
detected package name instead of the hardcoded string.
---
Nitpick comments:
In @.aiox-core/core/pro/pro-updater.js:
- Around line 233-242: The updatePro(public API) is missing validation for its
projectRoot parameter; add an early guard in the updatePro function to ensure
projectRoot is a non-empty string that resolves to an existing directory and
throw a clear, descriptive error (e.g., TypeError or custom Error) if not. Use
the Node fs APIs (fs.stat or fs.promises.stat) to test existence and
directory-ness before any async work begins, normalize the path (e.g.,
path.resolve) for clearer error messages, and reference updatePro and the
projectRoot parameter in the thrown message so callers immediately know the
input was invalid.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 152165d0-7938-44a5-aceb-435dfddbd351
📒 Files selected for processing (3)
.aiox-core/core/pro/pro-updater.js.aiox-core/install-manifest.yamltests/pro/pro-updater.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
- .aiox-core/install-manifest.yaml
- tests/pro/pro-updater.test.js
Resolves dual-scope package resolution for AIOX Pro. The canonical name @aiox-fullstack/pro does not yet exist on npm (pending org rename). The active fallback is @aios-fullstack/pro@0.3.0. All Pro-related code now tries canonical first, then fallback. 80/80 Pro tests pass.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests