Skip to content

Neos 9 compatibility (event-sourced Content Repository) — new 4.x line - #28

Merged
gradinarufelix merged 21 commits into
mainfrom
feature/neos9-compatibility
Aug 4, 2026
Merged

gradinarufelix merged 21 commits into
mainfrom
feature/neos9-compatibility

Conversation

@gradinarufelix

@gradinarufelix gradinarufelix commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Migrates NEOSidekick.AiAssistant to Neos 9.0/9.1 and its event-sourced Content
Repository. This branch is a clean fork of main and becomes the 4.x release line
(main was renamed to 4.x on merge); the 3.x branch continues to serve Neos 8.3/8.4. All decisions made during the migration
are additionally documented in a decision log (available on request — it was kept in
the demo project used for verification).

Supported: Neos 9.0 / 9.1, PHP 8.3 / 8.4 · Dropped: Neos 8.x support on this line

Review guide

The history is ordered so mechanical and manual changes stay separable:

Commits Content Review effort
Require Neos 9 packages… composer constraints; drops sitegeist/fusionlinkprototypes (no Neos 9 release; core Neos.Fusion:ActionUri replaces it) low
Apply neos/rector… purely automated Rector output (CONTENTREPOSITORY_9_0 set), no manual edits skim
Manual Neos 9 migration… the core rewrite: NodeData/Context queries → ContentGraph subgraphs, mutations → CR commands, LinkingService → NodeUriBuilder, Fusion module URIs, dimension access via ContentDimensionSource high
Test suite commits functional suite rewritten for the event-sourced CR (commands, NodeAddress assertions, distribution-agnostic language handling) medium
Bugfix commits issues found during end-to-end verification, each with its own rationale in the commit message medium

Key design decisions

  • Node identity towards the JS frontend is now NodeAddress JSON (workspace +
    dimensions + aggregate id), round-tripped as an opaque string — the Neos 9
    equivalent of the old context paths. Endpoints accept plain aggregate ids too.
  • Single-CR assumption centralized: all content-repository access goes through
    ContentRepositoryProvider, configurable via NEOSidekick.AiAssistant.contentRepositoryId
    (default default). Multi-CR support later is a one-place change.
  • Fallback dimensions are not editable rows (also backported to main for 2.x):
    important-pages re-addresses fallback URLs to their origin variant; writes to
    fallback addresses are rejected instead of materializing a variant as a save side
    effect. Variant creation stays an explicit editor decision in the Neos UI.
  • Patch application is no longer transactional (event-sourced CR has no rollback);
    all patches are validated up front, dryRun stops after validation.
  • Workspace visibility follows Neos 9 semantics: a node disabled in a user
    workspace is now correctly excluded from that workspace's results (the 2.x suite
    documented the old SQL-based behavior as a known quirk).

Notable fixes found during verification

  • Neos.Ui.Workspace Eel helper is no longer in Fusion's default context in Neos 9 —
    registered by the plugin, backend modules broke without it.
  • UserService::getPersonalWorkspaceName() is a throwing stub in Neos 9 — all five
    agent API endpoints 500ed; replaced by a PersonalWorkspaceService (WorkspaceService
    metadata, old null contract preserved).
  • Important-pages sent host/<uriSegment> entry URLs; in Neos 9 the site-default
    language's homepage has an empty URI path, so the NEOSidekick API crawled a 404 and
    reported the site as "not publicly accessible". Entry URLs are now router-generated
    homepage URIs per language.
  • Every await in the generate chain that could pend forever is now bounded
    (guest-frame polling, API fetch, image metadata) and surfaces a flash message
    instead of an endless spinner (backported to main).

Upgrade notes for site integrators (also in README)

  • New optional setting contentRepositoryId (default default).
  • Per-dimension-value options (e.g. Sitegeist.LostInTranslation translationStrategy)
    must move with the dimension configuration into the CR registry settings.
  • JS stack: @neos-project/* 9.1.6, FontAwesome 6; bundles rebuilt.

Testing

  • CI: matrix Neos 9.0 (~9.0.0) + 9.1 (pinned 9.1.6 — 9.1.7/9.1.8 broke the
    install step in CI, see commit message; unpin after investigation) × PHP 8.3/8.4,
    with a MariaDB service container (the content graph does not support SQLite —
    mirrors neos-development-collection's build.yml).
  • Functional: 27 tests / 282 assertions, rewritten for the event-sourced CR;
    distribution-agnostic (adapts to the host's content dimensions, skips
    dimension-variant tests where fewer than two languages exist). Requires a
    MariaDB/MySQL test database (documented in TESTING.md).
  • Unit: 21 tests. phpcs (PSR-12) and phpstan clean.
  • Manually verified end-to-end on a Neos 9.1.6 demo instance (Neos.Demo) with the
    real NEOSidekick API: inspector editors (focus keyword, SEO title, image alt text),
    all six backend modules incl. bulk generate & save, chat sidebar incl. the full
    agent authorization flow, and agent tools (SEO metadata, CTA, structural patches).

Known follow-ups

  • Unpin Neos 9.1 in CI once 9.1.7/9.1.8 are investigated on a runner.
  • Agent API endpoints (node-tree, document-list, preview, apply-patches, search) have
    no functional-test coverage with a stubbed JWT identity — this blind spot is where
    the getPersonalWorkspaceName() 500 hid.
  • NodeVisibility wraps the Neos 9 visibility constraints; revisit before Neos 10.
  • React stays 16-typed per react-ui-components 9.1.6 peer range; the host UI
    provides React at runtime via the extensibility alias map.

Summary by CodeRabbit

  • New Features

    • Added compatibility with Neos 9.0 and 9.1.
    • Improved support for workspaces, content dimensions, language variants, previews, routing, search, and node management.
    • Added support for configurable content repositories and MariaDB-based functional testing.
  • Bug Fixes

    • Improved handling of invalid nodes, missing workspaces, hidden content, fallback dimensions, and image metadata.
  • Documentation

    • Expanded Neos 9 setup, migration, content repository, and testing guidance.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@gradinarufelix, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: daade561-99ed-47e5-a9ac-1d621dd004ed

📥 Commits

Reviewing files that changed from the base of the PR and between 76be352 and 4163a57.

📒 Files selected for processing (2)
  • .github/workflows/test.yml
  • README.md
📝 Walkthrough

Walkthrough

This PR migrates the extension to Neos 9 Content Repository APIs. It updates graph reads, event-sourced patch commands, workspace handling, Fusion integration, functional tests, dependencies, CI, and compatibility documentation.

Changes

Neos 9 repository integration

Layer / File(s) Summary
Repository and workspace wiring
Classes/Service/ContentRepositoryProvider.php, Classes/Service/PersonalWorkspaceService.php, Classes/Controller/*, Configuration/*, Resources/Private/BackendModule/*
Content repository and personal workspace resolution now use Neos 9 services. Controllers, Fusion endpoints, dimensions, visibility constraints, and node-type access use updated APIs.
Content graph read paths
Classes/Service/*, Classes/Factory/*, Classes/EelHelper/*
Node listing, searching, preview rendering, URI generation, image extraction, tree extraction, and node filtering now use content graphs, subgraphs, typed nodes, dimensions, and node addresses.
Patch validation and execution
Classes/Service/NodePatchService.php, Classes/Service/PatchValidator.php
Patch validation now uses content subgraphs. Create, update, move, delete, reference, visibility, dry-run, and failure handling now dispatch Neos 9 Content Repository commands.
Functional test migration
Tests/Functional/*
Functional setup, content creation, language handling, workspace publication, routing, node addresses, and service assertions now use Neos 9 commands and graph helpers.
Compatibility and validation support
.github/workflows/test.yml, composer.json, README.md, TESTING.md, Resources/Private/NeosUserInterface/*
CI tests Neos 9.0 and 9.1 with MariaDB. Dependencies, UI packages, documentation, and testing configuration target Neos 9.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: rolandschuetz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Neos 9 compatibility through the event-sourced Content Repository in a new 4.x line.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/neos9-compatibility

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 18

🧹 Nitpick comments (14)
Classes/Service/PatchValidator.php (2)

375-388: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validation and execution build the TransientNode differently for updates.

Here updates use NodeAggregateIdsByNodePaths::createForNodeType(...), while NodePatchService::executeUpdateNode() uses createEmpty(). Divergent transient-node setup can make a patch pass pre-validation and then fail during execution (partial application, no rollback). Worth aligning both paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/PatchValidator.php` around lines 375 - 388, Align the update
validation setup in the code surrounding TransientNode::forRegular with
NodePatchService::executeUpdateNode(): use
NodeAggregateIdsByNodePaths::createEmpty() instead of createForNodeType(...).
Keep the existing setup for non-update validation paths unchanged.

223-240: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Unknown node types silently skip the move constraint check.

Unlike validateCreateNodePatch/validateUpdateNodePatch, which throw when a type is not in the schema, this condition just bypasses validation when either type resolves to null. Prefer an explicit failure for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/PatchValidator.php` around lines 223 - 240, Update the node
type validation in the move validation flow to explicitly throw a
PatchFailedException when either $newParentNodeType or $nodeType is null,
matching the unknown-type behavior of validateCreateNodePatch and
validateUpdateNodePatch. Only perform allowsChildNodeType validation after both
node types resolve successfully, preserving the existing rejection for
disallowed child types.
Classes/Service/NodePatchService.php (2)

740-766: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Node-identifier parsing duplicated across the patch services. Both services independently parse nodeId as either a plain NodeAggregateId or a NodeAddress JSON string and resolve it in the subgraph; the accepted formats can drift between validation and execution.

  • Classes/Service/NodePatchService.php#L740-L766: replace the inline parsing in requireNode() with a call to a shared resolver helper.
  • Classes/Service/PatchValidator.php#L297-L322: replace the identical parsing in getNodeById() with the same shared resolver.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodePatchService.php` around lines 740 - 766, Extract the
duplicated nodeId parsing and subgraph lookup into a shared resolver helper,
then update NodePatchService::requireNode() in
Classes/Service/NodePatchService.php lines 740-766 to use it while preserving
its existing PatchFailedException handling. Update PatchValidator::getNodeById()
in Classes/Service/PatchValidator.php lines 297-322 to use the same helper,
ensuring both services accept identical plain NodeAggregateId and NodeAddress
JSON formats.

268-277: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use hasNodeType() before getNodeType(), as PatchValidator does.

PatchValidator::getNodeType() documents that getNodeType() returns a fallback node type rather than null when a fallback is configured, so this null-check can pass for a non-existent type. Same guard should apply here.

♻️ Proposed change
             $nodeTypeManager = $contentRepository->getNodeTypeManager();
-            $nodeType = $nodeTypeManager->getNodeType($patch->getNodeType());
-            if ($nodeType === null) {
+            $nodeType = $nodeTypeManager->hasNodeType($patch->getNodeType())
+                ? $nodeTypeManager->getNodeType($patch->getNodeType())
+                : null;
+            if ($nodeType === null) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodePatchService.php` around lines 268 - 277, Update the node
type validation in the patch handling method containing $nodeTypeManager to call
hasNodeType($patch->getNodeType()) before getNodeType(). Throw the existing
PatchFailedException when the type is absent, then retrieve it with
getNodeType() only after validation; do not rely on a null check because
fallback configuration can return a node type for unknown names.
Tests/E2E/tests/neos9.editorsAndChat.spec.ts (1)

19-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type the page parameter.

Implicit any here loses autocompletion and breaks under noImplicitAny if the suite is ever type-checked.

♻️ Proposed tweak
-import { test, expect } from '`@playwright/test`';
+import { test, expect, type Page } from '`@playwright/test`';
@@
-const dockSidebar = async (page) => {
+const dockSidebar = async (page: Page) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/E2E/tests/neos9.editorsAndChat.spec.ts` around lines 19 - 23, Type the
page parameter in dockSidebar using the appropriate Playwright Page type already
used by the test suite, preserving the existing initialization behavior.
Tests/Functional/FunctionalTestCase.php (1)

360-397: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the created document is resolvable before returning.

findNodeById() is nullable while the method is declared : Node; a projection miss would surface as a TypeError instead of a readable failure, unlike the tethered-child check on Line 381.

♻️ Proposed tweak
-        return $subgraph->findNodeById($documentNodeAggregateId);
+        $documentNode = $subgraph->findNodeById($documentNodeAggregateId);
+        $this->assertNotNull($documentNode, sprintf('Document node "%s" was not created', $nodeName));
+
+        return $documentNode;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Functional/FunctionalTestCase.php` around lines 360 - 397, Update
createPageWithImageNodes to assert that findNodeById($documentNodeAggregateId)
returns a non-null Node before returning it, matching the existing assertion for
the tethered main collection and producing a readable failure on projection
misses.
composer.json (1)

18-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid dev-master as a production fallback.

^9.0 || dev-master allows future Neos development code to satisfy the dependency and can silently introduce incompatible APIs. Composer explicitly warns that unbounded constraints such as dev-master allow updates to future versions. Keep the stable constraint here, or isolate the development constraint in an explicit compatibility-testing profile. (getcomposer.org)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composer.json` around lines 18 - 20, Remove the dev-master alternatives from
the neos/neos, neos/media, and neos/neos-ui dependency constraints in
composer.json, leaving only the stable ^9.0 requirements. Do not add an
unbounded development fallback; isolate any development compatibility constraint
separately if needed.

Source: MCP tools

Classes/Service/NodeWithImageService.php (1)

99-104: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Language filter silently passes documents without the language coordinate.

When $languageCoordinate === null the document is kept even though a language filter was requested. If that is intentional (dimension-less installations), a short comment would help; otherwise it lets unfiltered documents through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodeWithImageService.php` around lines 99 - 104, Update the
language-filter logic in NodeWithImageService to exclude documents when a
language filter is requested and getCoordinate returns null, while preserving
documents whose coordinate matches the filter. If null is intentionally allowed
for dimension-less installations, document that behavior with a concise comment
instead.
Classes/Service/NodeTreeExtractor.php (1)

42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two overlapping content-repository dependencies.

extractChildren() resolves the CR from $node->contentRepositoryId via the registry while extract() uses contentRepositoryProvider. Since the node always originates from the provider's repository, dropping the registry injection (or the provider) would keep one source of truth; contentRepositoryRegistry->subgraphForNode($node) can stay.

Also applies to: 228-234

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodeTreeExtractor.php` around lines 42 - 45, Consolidate the
content-repository dependency in NodeTreeExtractor by removing the redundant
injection and updating extract() and extractChildren() to use one consistent
repository source. Preserve the existing subgraphForNode($node) behavior while
ensuring both methods resolve the repository associated with the provider’s
node.
Classes/Service/DocumentNodeListExtractor.php (1)

115-135: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused $contentRepository parameter in resolveSiteNode().

Flagged by PHPMD; the parameter is never read in the method body.

🧹 Suggested cleanup
-    private function resolveSiteNode(ContentRepository $contentRepository, ContentSubgraphInterface $subgraph, ?string $siteNodeName): ?Node
+    private function resolveSiteNode(ContentSubgraphInterface $subgraph, ?string $siteNodeName): ?Node
     {

And update the call site: $this->resolveSiteNode($subgraph, $siteNodeName).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/DocumentNodeListExtractor.php` around lines 115 - 135, Remove
the unused ContentRepository parameter from resolveSiteNode() and update its
call site to pass only $subgraph and $siteNodeName; preserve the existing
node-resolution behavior.

Source: Linters/SAST tools

Classes/Controller/PreviewRenderController.php (1)

167-207: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Legacy-dimensions-to-DimensionSpacePoint mapping duplicated across two files. Both implementations independently convert a legacy fallback-chain dimensions array into a DimensionSpacePoint, using the primary/first value per dimension and falling back to the variation graph's root generalization when no dimensions are given — the same root cause (no shared helper) as NodeVisibility.php, which centralizes a similar cross-cutting Neos 9 concern.

  • Classes/Controller/PreviewRenderController.php#L167-L207: extract dimensionCoordinatesFromLegacyDimensionsArray() + getDefaultDimensionSpacePoint() into a shared service (e.g. alongside NodeVisibility) and delegate to it here.
  • Classes/Service/DocumentNodeListExtractor.php#L208-L220: replace resolveDimensionSpacePoint()'s body with a call to the same shared helper instead of reimplementing the mapping.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Controller/PreviewRenderController.php` around lines 167 - 207,
Extract the legacy dimension-coordinate mapping and variation-graph default
selection from PreviewRenderController methods
dimensionCoordinatesFromLegacyDimensionsArray() and
getDefaultDimensionSpacePoint() into a shared service alongside NodeVisibility,
preserving primary fallback values and root-generalization behavior. Delegate
PreviewRenderController to that service, and replace
resolveDimensionSpacePoint() in Classes/Service/DocumentNodeListExtractor.php
lines 208-220 with the same helper call; both sites should use the shared
implementation rather than duplicate the logic.
Classes/Service/NodeService.php (1)

203-241: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

updatePropertiesOnNode() propagates raw parse failures to the API caller.

NodeAddress::fromJsonString() on Line 227 is unguarded, so a malformed nodeContextPath from the client surfaces as an unhandled exception rather than the InvalidArgumentException contract used two lines below for the missing-node case. Wrapping it keeps the failure mode uniform for ApplyPatchesApi/BackendService consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodeService.php` around lines 203 - 241, Update
updatePropertiesOnNode() to catch parse failures from
NodeAddress::fromJsonString() and rethrow them as InvalidArgumentException,
preserving the original error as the cause and using the same client-facing
contract as the missing-node branch. Leave valid address processing and the
existing missing-node exception unchanged.
Classes/Factory/FindImageDataFactory.php (1)

98-115: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

resolveNodeOrderIndex() adds two graph queries per image node.

createFromNodeAndSchema() is invoked once per matching content node in NodeWithImageService::findDocumentNodesHavingChildNodesWithImages(), which walks every descendant of every document. Each call does a findParentNode() plus a full findChildNodes() fetch of the sibling set, so a page with many image nodes re-fetches the same sibling list repeatedly. A per-request memo keyed by parent aggregate id + subgraph identity removes the repetition without changing semantics.

♻️ Memoize sibling positions per parent
+    /** `@var` array<string, array<string, int>> */
+    private array $orderIndexCache = [];
+
     private function resolveNodeOrderIndex(Node $node): int
     {
         $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node);
         $parentNode = $subgraph->findParentNode($node->aggregateId);
         if ($parentNode === null) {
             return 0;
         }
 
-        $position = 0;
-        foreach ($subgraph->findChildNodes($parentNode->aggregateId, FindChildNodesFilter::create()) as $siblingNode) {
-            if ($siblingNode->aggregateId->equals($node->aggregateId)) {
-                return $position;
-            }
-            $position++;
-        }
-
-        return 0;
+        $cacheKey = $node->workspaceName->value . '|' . $node->dimensionSpacePoint->hash . '|' . $parentNode->aggregateId->value;
+        if (!isset($this->orderIndexCache[$cacheKey])) {
+            $positions = [];
+            $position = 0;
+            foreach ($subgraph->findChildNodes($parentNode->aggregateId, FindChildNodesFilter::create()) as $siblingNode) {
+                $positions[$siblingNode->aggregateId->value] = $position++;
+            }
+            $this->orderIndexCache[$cacheKey] = $positions;
+        }
+
+        return $this->orderIndexCache[$cacheKey][$node->aggregateId->value] ?? 0;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Factory/FindImageDataFactory.php` around lines 98 - 115, Memoize
sibling positions in resolveNodeOrderIndex() to avoid repeating findParentNode()
and findChildNodes() queries for image nodes sharing the same parent and
subgraph. Add a per-request cache keyed by the parent aggregate ID and subgraph
identity, populate it once from the sibling list, and reuse the cached node
positions while preserving the existing zero fallback semantics.
Classes/Service/AssetService.php (1)

74-91: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Dropping AssetRepository::iterate() also drops its periodic identity-map cleanup.

Flow's iterate() detached/cleared hydrated entities every N rows; plain toIterable() keeps every hydrated Image in the Doctrine identity map for the whole loop. Combined with the offset emulation ($iteratedItems <= getFirstResult()), a deep page over a large media library now hydrates and retains all skipped rows. Consider detaching skipped assets, or moving offset/limit into the query itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/AssetService.php` around lines 74 - 91, Update the asset
iteration in the service method using $assetsIterator so deep pagination does
not retain every skipped entity in Doctrine’s identity map. Prefer applying the
first-result offset and limit to the query before iteration; otherwise detach
skipped $currentAsset entities while preserving the existing usage filtering and
result-count behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Line 50: Update the setup-php configuration’s extensions list to replace the
removed mysql extension with pdo_mysql, matching the workflow’s pdo_mysql driver
requirement, and add fail-fast: true under that setup step so installation
failures stop the job immediately.

In `@Classes/Controller/PreviewRenderController.php`:
- Around line 150-165: The findNode() method currently fetches its subgraph
through contentRepository->getContentSubgraph(), allowing authorization-derived
visibility. Replace that fetch with the package’s getSubgraph() path and
explicitly pass NodeVisibility::excludeRemoved(), while preserving the existing
workspace, dimension-space point, node lookup, and exception handling.

In `@Classes/EelHelper/NEOSidekickInternalHelper.php`:
- Around line 100-104: Update contentDimensionsByName() to stop instantiating
the legacy Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper and retrieve the
dimensions through the supported Neos\Neos\Fusion\Helper\DimensionHelper or
equivalent Neos.Dimension Eel helper API, preserving the existing content
repository context and array result.

In `@Classes/Service/NodePatchService.php`:
- Around line 357-375: Reorder the hidden-node creation flow so
`$subgraph->findNodeById($newNodeAggregateId)` and its null check occur before
dispatching `DisableNodeAggregate`. Keep the existing disable operation
afterward, ensuring hidden nodes are validated while visible and avoiding
dependence on subgraph visibility constraints.
- Around line 160-174: Update the Apply Patches response documentation in
ApplyPatchesApiController.php and README_Internal_API.md to reflect dry-run
results containing only index, operation, and nodeId. Remove createdNodes and
any previewed node-detail examples from the dry-run response documentation while
preserving the documented non-dry-run response behavior.

In `@Classes/Service/NodeService.php`:
- Around line 312-326: Update nodeMatchesLanguageDimensionFilter() to return
true when getLanguageDimensionFilter() is empty, treating an unset language
filter as no restriction. Preserve the existing dimension-source and coordinate
checks for non-empty filters so find() and findImportantPages() behave
consistently.
- Around line 82-104: In findImportantPages, normalize the nullable result of
FindDocumentNodesFilter::getLanguageDimensionFilter() to an empty array once
before the language-dimension loop, then use that normalized variable for the
empty check and membership filtering. Apply the same normalization in the other
in_array usage identified in the diff, preserving existing behavior for non-null
filters.

In `@Classes/Service/NodeTreeExtractor.php`:
- Around line 59-64: Update the subgraph lookup in NodeTreeExtractor to resolve
an empty dimensions array through the existing variation-graph
root-generalization fallback used by
SearchNodesExtractor::resolveDimensionSpacePoint(), rather than passing an empty
DimensionSpacePoint to getContentSubgraph. Apply the same change to the
additional lookup path around the referenced lines while preserving explicit
dimension coordinates.

In `@Classes/Service/NodeWithImageService.php`:
- Around line 75-95: Update NodeWithImageService to reference the global
InvalidArgumentException explicitly (or import it) for both the
missing-workspace throw and the NodeAddress::fromJsonString() catch. Verify
fromJsonString() throws PHP’s \InvalidArgumentException in Neos 9; if it uses a
different exception type, catch that exact type instead so malformed addresses
are skipped and logged.

In `@Classes/Service/SearchNodesExtractor.php`:
- Around line 167-172: Update resolveNodeByIdentifier() to normalize
$effectiveNodeTypeFilter using the same NodeTypeCriteria parsing as
findDescendantNodes() before calling NodeType::isOfType(). Preserve support for
comma-separated and !* filters, and use the normalized criteria when evaluating
the node-type condition.

In `@README.md`:
- Around line 18-21: Update the README installation example to require the Neos
9-compatible plugin major, replacing the neosidekick/ai-assistant ^2.5
constraint with ^3.0 or an equivalent version-neutral constraint consistent with
the documented release availability.

In `@Resources/Private/BackendModule/BackendModule.fusion`:
- Around line 74-81: Update the workspaces value in the Neos.Fusion:Value block
to use the workspace value object that provides the configured workspace data,
rather than relying on
Neos.Ui.Workspace.getPersonalWorkspace(contentRepositoryId). Preserve the
allowedTargetWorkspaces mapping and JSON serialization so props.workspaces
contains the personal and allowed workspaces for switcher initialization.

In `@TESTING.md`:
- Around line 65-73: Update TESTING.md lines 65-73 to remove the unconditional
“ANY Neos 9 site” and “works out of the box” claims, and state that eligibility
follows each spec’s requirements, including a publicly reachable base URL and an
image asset with an empty title for neos9.bulkAltTextModule.spec.ts. Update
Tests/E2E/README.md lines 34-39 to remove “without extra content” and make Neos
9 E2E eligibility conditional on the documented per-spec requirements.

In `@Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts`:
- Around line 47-55: Replace the fixed page.waitForTimeout(5_000) after
saveAll.click() with an explicit wait for the save completion signal, such as
the updateAssets response, confirmation UI, next-page navigation, or saved-state
UI. Keep the existing no-alert assertion and ensure they run only after the save
has completed.
- Around line 36-39: Update the persistence check around firstImageSrc to
capture a stable identifier for the asset being edited instead of relying on the
first image source. After saving, explicitly wait for the post-save list state
using an assertion for the empty state or rendered list, then verify that
specific asset is absent or its persisted title is present; do not use immediate
isVisible checks or list ordering as the synchronization/identity mechanism.

In `@Tests/E2E/tests/neos9.editorsAndChat.spec.ts`:
- Around line 74-104: The chat-readiness check using chatInput().isVisible({
timeout: 30_000 }) is a one-shot check and should use a retry-based wait
instead. Update the chat input readiness flow to call waitFor({ state:
'visible', timeout: 30_000 }) or expect(...).toBeVisible(...) with the existing
catch-to-false behavior, while preserving the page.reload() fallback when the
wait fails.

In `@Tests/Functional/Service/NodeServiceImportantPagesMultiDomainTest.php`:
- Around line 35-42: The test expectation for
getMostRelevantInternalSeoUrisByHosts must match
NodeService::findImportantPages() when no language dimension exists. In the
relevant test method, either skip the dimension-dependent test when
primaryLanguage() is null or construct expected hosts without a trailing slash
in that case, while preserving the current language-segment expectations for
dimension-enabled projects.

In
`@Tests/Functional/Service/NodeServiceWithImportantPagesFilterAndMultipleDimensionsAndOneSiteTest.php`:
- Around line 69-72: Update the forbidden-node assertion in the test’s
$foundNodes verification to compare the result-array key correctly. Replace the
per-DTO getNodeContextPath() comparison with assertArrayNotHasKey($forbiddenKey,
$foundNodes), preserving the existing count assertion and test intent.

---

Nitpick comments:
In `@Classes/Controller/PreviewRenderController.php`:
- Around line 167-207: Extract the legacy dimension-coordinate mapping and
variation-graph default selection from PreviewRenderController methods
dimensionCoordinatesFromLegacyDimensionsArray() and
getDefaultDimensionSpacePoint() into a shared service alongside NodeVisibility,
preserving primary fallback values and root-generalization behavior. Delegate
PreviewRenderController to that service, and replace
resolveDimensionSpacePoint() in Classes/Service/DocumentNodeListExtractor.php
lines 208-220 with the same helper call; both sites should use the shared
implementation rather than duplicate the logic.

In `@Classes/Factory/FindImageDataFactory.php`:
- Around line 98-115: Memoize sibling positions in resolveNodeOrderIndex() to
avoid repeating findParentNode() and findChildNodes() queries for image nodes
sharing the same parent and subgraph. Add a per-request cache keyed by the
parent aggregate ID and subgraph identity, populate it once from the sibling
list, and reuse the cached node positions while preserving the existing zero
fallback semantics.

In `@Classes/Service/AssetService.php`:
- Around line 74-91: Update the asset iteration in the service method using
$assetsIterator so deep pagination does not retain every skipped entity in
Doctrine’s identity map. Prefer applying the first-result offset and limit to
the query before iteration; otherwise detach skipped $currentAsset entities
while preserving the existing usage filtering and result-count behavior.

In `@Classes/Service/DocumentNodeListExtractor.php`:
- Around line 115-135: Remove the unused ContentRepository parameter from
resolveSiteNode() and update its call site to pass only $subgraph and
$siteNodeName; preserve the existing node-resolution behavior.

In `@Classes/Service/NodePatchService.php`:
- Around line 740-766: Extract the duplicated nodeId parsing and subgraph lookup
into a shared resolver helper, then update NodePatchService::requireNode() in
Classes/Service/NodePatchService.php lines 740-766 to use it while preserving
its existing PatchFailedException handling. Update PatchValidator::getNodeById()
in Classes/Service/PatchValidator.php lines 297-322 to use the same helper,
ensuring both services accept identical plain NodeAggregateId and NodeAddress
JSON formats.
- Around line 268-277: Update the node type validation in the patch handling
method containing $nodeTypeManager to call hasNodeType($patch->getNodeType())
before getNodeType(). Throw the existing PatchFailedException when the type is
absent, then retrieve it with getNodeType() only after validation; do not rely
on a null check because fallback configuration can return a node type for
unknown names.

In `@Classes/Service/NodeService.php`:
- Around line 203-241: Update updatePropertiesOnNode() to catch parse failures
from NodeAddress::fromJsonString() and rethrow them as InvalidArgumentException,
preserving the original error as the cause and using the same client-facing
contract as the missing-node branch. Leave valid address processing and the
existing missing-node exception unchanged.

In `@Classes/Service/NodeTreeExtractor.php`:
- Around line 42-45: Consolidate the content-repository dependency in
NodeTreeExtractor by removing the redundant injection and updating extract() and
extractChildren() to use one consistent repository source. Preserve the existing
subgraphForNode($node) behavior while ensuring both methods resolve the
repository associated with the provider’s node.

In `@Classes/Service/NodeWithImageService.php`:
- Around line 99-104: Update the language-filter logic in NodeWithImageService
to exclude documents when a language filter is requested and getCoordinate
returns null, while preserving documents whose coordinate matches the filter. If
null is intentionally allowed for dimension-less installations, document that
behavior with a concise comment instead.

In `@Classes/Service/PatchValidator.php`:
- Around line 375-388: Align the update validation setup in the code surrounding
TransientNode::forRegular with NodePatchService::executeUpdateNode(): use
NodeAggregateIdsByNodePaths::createEmpty() instead of createForNodeType(...).
Keep the existing setup for non-update validation paths unchanged.
- Around line 223-240: Update the node type validation in the move validation
flow to explicitly throw a PatchFailedException when either $newParentNodeType
or $nodeType is null, matching the unknown-type behavior of
validateCreateNodePatch and validateUpdateNodePatch. Only perform
allowsChildNodeType validation after both node types resolve successfully,
preserving the existing rejection for disallowed child types.

In `@composer.json`:
- Around line 18-20: Remove the dev-master alternatives from the neos/neos,
neos/media, and neos/neos-ui dependency constraints in composer.json, leaving
only the stable ^9.0 requirements. Do not add an unbounded development fallback;
isolate any development compatibility constraint separately if needed.

In `@Tests/E2E/tests/neos9.editorsAndChat.spec.ts`:
- Around line 19-23: Type the page parameter in dockSidebar using the
appropriate Playwright Page type already used by the test suite, preserving the
existing initialization behavior.

In `@Tests/Functional/FunctionalTestCase.php`:
- Around line 360-397: Update createPageWithImageNodes to assert that
findNodeById($documentNodeAggregateId) returns a non-null Node before returning
it, matching the existing assertion for the tethered main collection and
producing a readable failure on projection misses.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e54fc496-b474-4c47-aef6-a1ee93e5aae9

📥 Commits

Reviewing files that changed from the base of the PR and between 583151c and e522e3b.

📒 Files selected for processing (43)
  • .github/workflows/test.yml
  • Classes/Controller/BackendModule/AbstractFusionViewController.php
  • Classes/Controller/BackendServiceController.php
  • Classes/Controller/PreviewRenderController.php
  • Classes/EelHelper/NEOSidekickHelper.php
  • Classes/EelHelper/NEOSidekickInternalHelper.php
  • Classes/Factory/FindDocumentNodeDataFactory.php
  • Classes/Factory/FindImageDataFactory.php
  • Classes/Service/AbstractNodeService.php
  • Classes/Service/AssetService.php
  • Classes/Service/ContentRepositoryProvider.php
  • Classes/Service/DocumentNodeListExtractor.php
  • Classes/Service/NodeFindingService.php
  • Classes/Service/NodePatchService.php
  • Classes/Service/NodeService.php
  • Classes/Service/NodeTreeExtractor.php
  • Classes/Service/NodeTypeSchemaExtractor.php
  • Classes/Service/NodeTypeService.php
  • Classes/Service/NodeVisibility.php
  • Classes/Service/NodeWithImageService.php
  • Classes/Service/PatchValidator.php
  • Classes/Service/PropertyNormalizer.php
  • Classes/Service/SearchNodesExtractor.php
  • Classes/Service/SiteService.php
  • Classes/Service/Traits/PropertyExtractionTrait.php
  • Configuration/Settings.Internal.yaml
  • Configuration/Settings.yaml
  • Configuration/Testing/NodeTypes.HomePage.yaml
  • Configuration/Testing/Settings.yaml
  • README.md
  • Resources/Private/BackendModule/BackendModule.fusion
  • Resources/Private/BackendModule/Root.fusion
  • TESTING.md
  • Tests/E2E/README.md
  • Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts
  • Tests/E2E/tests/neos9.editorsAndChat.spec.ts
  • Tests/Functional/FunctionalTestCase.php
  • Tests/Functional/Service/NodeServiceImportantPagesMultiDomainTest.php
  • Tests/Functional/Service/NodeServiceMultiSiteTest.php
  • Tests/Functional/Service/NodeServiceWithImportantPagesFilterAndMultipleDimensionsAndOneSiteTest.php
  • Tests/Functional/Service/NodeServiceWithMultipleDimensionsAndOneSiteTest.php
  • Tests/Functional/Service/NodeServiceWorkspacesTest.php
  • composer.json
💤 Files with no reviewable changes (3)
  • Resources/Private/BackendModule/Root.fusion
  • Classes/Service/SiteService.php
  • Classes/Controller/BackendServiceController.php

with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql, pgsql, redis, memcached, memcache, apcu
extensions: mbstring, xml, json, zlib, iconv, intl, mysql, pgsql, redis, memcached, memcache, apcu

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

printf 'Tracked workflow files:\n'
git ls-files '.github/workflows/*'

printf '\nRelevant setup-php lines:\n'
if [ -f .github/workflows/test.yml ]; then
  nl -ba .github/workflows/test.yml | sed -n '1,110p'
fi

printf '\nSearch for pdo/mysql references in workflow and composer/config:\n'
rg -n "pdo|mysql|driver:\s*pdo|extensions:|setup-php|fail-fast" -S .github composer.json src 2>/dev/null || true

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 1064


🌐 Web query:

shivammathur/setup-php driver pdo_mysql extensions pdo_mysql fail-fast env

💡 Result:

To configure the pdo_mysql extension in the shivammathur/setup-php GitHub Action and control how the workflow responds to installation failures, you should use the extensions input and the fail-fast environment variable [1][2]. Configuration Example: - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.5' extensions: pdo_mysql env: fail-fast: true Extension Management: The extensions input accepts a comma-separated list of extensions [3][4]. By adding pdo_mysql to this list, the action will attempt to install and enable the driver [5][4]. Fail-Fast Behavior: By default, shivammathur/setup-php operates in a "graceful" mode, meaning if an extension (or tool) fails to install, the action will log a warning but allow the workflow to continue [2][6]. Setting the fail-fast environment variable to true (as shown in the example above) changes this behavior [1][2]. With fail-fast: true, the workflow will immediately mark the step as failed if any specified extension or tool cannot be successfully set up [1][2]. This is useful for ensuring that your CI/CD environment is strictly configured according to your project requirements [7].

Citations:


🌐 Web query:

PHP removed ext mysql pgsql extension name PDO mysql setup-php

💡 Result:

The original ext/mysql extension (often referred to as the mysql_* functions) was officially deprecated in PHP 5.5.0 and completely removed in PHP 7.0.0 [1][2][3]. It was removed because the API had become outdated, lacked support for modern features, and presented significant maintenance and security challenges [3]. PHP does not have an extension named ext/pgsql that has been removed in the same manner as the original mysql extension; PostgreSQL support is provided by the pgsql extension, which remains a part of the PHP ecosystem. To use MySQL with PHP today, you should use either the MySQLi (MySQL Improved) extension or the PDO_MySQL (PHP Data Objects) extension [1][2]. Setup for PDO_MySQL: 1. Installation: Ensure the pdo_mysql extension is installed. On many Linux distributions, this is often included in a package named php-mysql or similar [4][5]. If you are compiling from source, use the --with-pdo-mysql flag [4][5]. 2. Connection: Use the PDO class to connect to your database. You must provide a Data Source Name (DSN) that begins with the mysql: prefix [6][7]. Example usage: $dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4'; $username = 'your_username'; $password = 'your_password'; try { $pdo = new PDO($dsn, $username, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); echo "Connected successfully."; } catch (PDOException $e) { echo "Connection failed: ". $e->getMessage; } For more details, see the official PHP documentation on the PDO_MySQL driver [4] and the PDO connection page [8].

Citations:


Install the PDO MySQL driver explicitly.

The workflow uses driver: pdo_mysql, but requests the removed ext/mysql extension in setup-php. Use pdo_mysql and add fail-fast: true under the setup step so a driver-install failure stops the job instead of being ignored and surfacing later during Flow bootstrap.

🔧 Proposed fix
       - name: Setup PHP
         uses: shivammathur/setup-php@v2
         with:
           php-version: ${{ matrix.php-versions }}
-          extensions: mbstring, xml, json, zlib, iconv, intl, mysql, pgsql, redis, memcached, memcache, apcu
+          extensions: mbstring, xml, json, zlib, iconv, intl, pdo_mysql, pgsql, redis, memcached, memcache, apcu
           ini-values: date.timezone="Africa/Tunis", opcache.fast_shutdown=0, apc.enable_cli=on
+        env:
+          fail-fast: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
extensions: mbstring, xml, json, zlib, iconv, intl, mysql, pgsql, redis, memcached, memcache, apcu
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, xml, json, zlib, iconv, intl, pdo_mysql, pgsql, redis, memcached, memcache, apcu
ini-values: date.timezone="Africa/Tunis", opcache.fast_shutdown=0, apc.enable_cli=on
env:
fail-fast: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml at line 50, Update the setup-php configuration’s
extensions list to replace the removed mysql extension with pdo_mysql, matching
the workflow’s pdo_mysql driver requirement, and add fail-fast: true under that
setup step so installation failures stop the job immediately.

Source: MCP tools

Comment on lines +150 to 165
protected function findNode(string $nodeId, string $workspace, array $dimensionsArray): ?Node
{
if ($dimensionsArray === []) {
$dimensionsArray = $this->getDefaultDimensions();
}
$contentRepository = $this->contentRepositoryProvider->getContentRepository();

$context = $this->createContentContext($workspace, $dimensionsArray);
$node = $context->getNodeByIdentifier($nodeId);

if ($node === null) {
try {
if ($dimensionsArray === []) {
$dimensionSpacePoint = $this->getDefaultDimensionSpacePoint($contentRepository);
} else {
$dimensionSpacePoint = DimensionSpacePoint::fromArray($this->dimensionCoordinatesFromLegacyDimensionsArray($dimensionsArray));
}
$subgraph = $contentRepository->getContentSubgraph(WorkspaceName::fromString($workspace), $dimensionSpacePoint);
return $subgraph->findNodeById(NodeAggregateId::fromString($nodeId));
} catch (\InvalidArgumentException | \Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist $e) {
return null;
}

$site = $this->findSiteForNode($node);
if ($site === null) {
return $node;
}

$contextProperties = $node->getContext()->getProperties();
$contextProperties['currentSite'] = $site;
if ($domain = $site->getFirstActiveDomain()) {
$contextProperties['currentDomain'] = $domain;
}
$siteContext = $this->_contextFactory->create($contextProperties);

return $siteContext->getNodeByIdentifier($nodeId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching PreviewRenderController.php and NodeVisibility.php"
fd -a 'PreviewRenderController.php|NodeVisibility\.php' . || true

echo
echo "Diff/stat context if available"
git diff --stat || true
git diff -- PreviewRenderController.php NodeVisibility.php 2>/dev/null | sed -n '1,220p' || true

echo
echo "Locate controller and helper paths"
controller=$(fd 'PreviewRenderController.php' . | head -n1 || true)
visibility=$(fd 'NodeVisibility.php' . | head -n1 || true)
echo "controller=$controller"
echo "visibility=$visibility"

if [ -n "${controller:-}" ]; then
  echo
  echo "Controller outline"
  ast-grep outline "$controller" --view expanded || true
  echo
  echo "Relevant controller lines"
  nl -ba "$controller" | sed -n '1,240p'
fi

if [ -n "${visibility:-}" ]; then
  echo
  echo "NodeVisibility helper"
  nl -ba "$visibility" | sed -n '1,220p'
fi

echo
echo "Search for visibility helpers and getContentSubgraph usage"
rg -n "getContentSubgraph|getSubgraph\\(|NodeVisibility|excludeRemoved|excludeDisabledAndRemoved|AccessDenied|withoutAuthorizationChecks|getContentRepository\\(|ContentRepositoryAuthProvider::getVisibilityConstraints" . || true

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 1334


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching PreviewRenderController.php and NodeVisibility.php"
fd -a 'PreviewRenderController.php|NodeVisibility\.php' . || true

echo
echo "Diff/stat context if available"
git diff --stat || true
git diff -- PreviewRenderController.php NodeVisibility.php 2>/dev/null | sed -n '1,220p' || true

echo
echo "Locate controller and helper paths"
controller=$(fd 'PreviewRenderController.php' . | head -n1 || true)
visibility=$(fd 'NodeVisibility.php' . | head -n1 || true)
echo "controller=$controller"
echo "visibility=$visibility"

if [ -n "${controller:-}" ]; then
  echo
  echo "Controller outline"
  ast-grep outline "$controller" --view expanded || true
  echo
  echo "Relevant controller lines"
  nl -ba "$controller" | sed -n '1,240p'
fi

if [ -n "${visibility:-}" ]; then
  echo
  echo "NodeVisibility helper"
  nl -ba "$visibility" | sed -n '1,220p'
fi

echo
echo "_SEARCH_REPLACE_FAILED_

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 1334


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Controller lines with read-line tool"
sed -n '1,230p' Classes/Controller/PreviewRenderController.php 2>/dev/null || true

echo
echo "NodeVisibility helper"
sed -n '1,220p' Classes/Service/NodeVisibility.php 2>/dev/null ||真

echo
echo "Relevant search terms"
rg -n "getContentSubgraph|getSubgraph\\(|NodeVisibility|excludeRemoved|excludeDisabledAndRemoved|AccessDenied|withoutAuthorizationChecks|getContentRepository\\(|ContentRepositoryAuthProvider::getVisibilityConstraints|previewToken|initialize" Classes config Package.php 2>/dev/null || true

echo
echo "Composer/package constraints"
if [ -f "composer.json" ]; then
  sed -n '1,220p' composer.json
fi
if [ -f "composer.lock" ]; then
  python3 - <<'PY'
import json
from pathlib import Path
p=Path('composer.lock')
if p.exists():
    data=json.loads(p.read_text())
for name in ['neos/contentrepository.core','neos/contentrepository','neos/neos']:
    for pkg in data.get('packages', [])+data.get('packages-dev', []):
        if pkg.get('name')==name:
            print(name, pkg.get('version'), pkg.get('source',{}).get('type'), pkg.get('source',{}).get('url'))
PY
fi

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 21314


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Controller lines with read-line tool"
sed -n '1,230p' Classes/Controller/PreviewRenderController.php 2>/dev/null || true

echo
echo "NodeVisibility helper"
sed -n '1,220p' Classes/Service/NodeVisibility.php 2>/dev/null || true

echo
echo "Relevant search terms"
rg -n "getContentSubgraph|getSubgroup\\(|getSubgraph\\(|NodeVisibility|excludeRemoved|excludeDisabledAndRemoved|AccessDenied|withoutAuthorizationChecks|getContentRepository\\(|ContentRepositoryAuthProvider::getVisibilityConstraints|previewToken|initialize" Classes config Package.php 2>/dev/null || true

echo
echo "Composer/package constraints"
if [ -f "composer.json" ]; then
  sed -n '1,220p' composer.json
fi
if [ -f "composer.lock" ]; then
  python3 - <<'PY'
import json
from pathlib import Path
p=Path('composer.lock')
if p.exists():
    data=json.loads(p.read_text())
for name in ['neosc/contentrepository.core','neos/contentrepository','neos/neos']:
    for pkg in data.get('packages', [])+data.get('packages-dev', []):
        if pkg.get('name')==name:
            print(name, pkg.get('version'), pkg.get('source',{}).get('type'), pkg.get('source',{}).get('url'))
PY
fi

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 21314


🌐 Web query:

Neos 9 ContentRepository getContentSubgraph VisibilityConstraints withoutAuthorizationChecks AccessDenied

💡 Result:

In Neos 9, the ContentRepository is designed to enforce security by default, which is why attempting to retrieve a ContentSubgraph without proper authorization can lead to an AccessDenied exception [1][2]. When you call $contentRepository->getContentSubgraph, the system implicitly uses the VisibilityConstraints of the current user, which are determined by the ContentRepositoryAuthorizationService based on the user's roles [1][3]. If you need to bypass these standard authorization checks, you can use the Flow SecurityContext's ability to disable authorization checks, or specifically manipulate the VisibilityConstraints [4][5]. 1. Disabling Authorization Checks: The Neos security system allows running code in a closure where authorization checks are disabled [5]. $this->securityContext->withoutAuthorizationChecks(function use ($contentRepository, $workspaceName, $dimensionSpacePoint) { $subgraph = $contentRepository->getContentSubgraph($workspaceName, $dimensionSpacePoint); //... }); 2. Using Specific VisibilityConstraints: Alternatively, you can manually define the VisibilityConstraints when requesting the subgraph to control what content is visible, such as using VisibilityConstraints::withoutRestrictions to fetch all content regardless of user permissions [6][7]. $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph( $dimensionSpacePoint, VisibilityConstraints::withoutRestrictions); Note that while bypassing checks is possible, developers should be cautious to check areAuthorizationChecksDisabled in custom code if needed, as the system does not always automatically handle every edge case, especially in CLI environments where the security context might not be fully initialized [5]. An AccessDenied exception generally indicates that the requested operation or data access violates the current user's privileges as enforced by the security framework [2][8][9].

Citations:


Use explicit frontend visibility when fetching preview nodes

findNode() intentionally disables authorization checks, but getContentSubgraph() still derives visibility from the auth provider. Since the package defines intent-revealing visibility helpers, use getSubgraph() with NodeVisibility::excludeRemoved() so preview behavior does not depend on Neos 9 patch/authorization implementation details.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Controller/PreviewRenderController.php` around lines 150 - 165, The
findNode() method currently fetches its subgraph through
contentRepository->getContentSubgraph(), allowing authorization-derived
visibility. Replace that fetch with the package’s getSubgraph() path and
explicitly pass NodeVisibility::excludeRemoved(), while preserving the existing
workspace, dimension-space point, node lookup, and exception handling.

Comment on lines +100 to +104
public function contentDimensionsByName(): array
{
$contentRepositoryId = $this->contentRepositoryProvider->getContentRepositoryId();
return (new \Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper())->contentDimensionsByName($contentRepositoryId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the installed helper for injected dependencies
fd -t f 'ContentDimensionsHelper.php' | xargs -r sed -n '1,80p'

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching helper/eel:"
fd -t f 'NEOSidekickInternalHelper.php|ContentDimensionsHelper.php' .

echo
echo "Git status/stat:"
git diff --stat 2>/dev/null || true
git status --short 2>/dev/null || true

echo
echo "Relevant class outline/content:"
if [ -f Classes/EelHelper/NEOSidekickInternalHelper.php ]; then
  wc -l Classes/EelHelper/NEOSidekickInternalHelper.php
  sed -n '1,160p' Classes/EelHelper/NEOSidekickInternalHelper.php
else
  echo "Classes/EelHelper/NEOSidekickInternalHelper.php not found"
fi

echo
echo "Search for ContentDimensionsDependency usage:"
rg -n "ContentDimensionsHelper|contentDimensionsByName|ContentRepositoryRegistry|contentRepositoryRegistry" . --glob '!vendor' --glob '!Build' --glob '!node_modules' || true

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 10150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Dependencies and lockfile package versions:"
fd -t f 'composer.json|composer.lock' . -x sh -c 'echo "--- $1"; sed -n "1,80p;180,260p" "$1"' sh {}

echo
echo "Check for Neos.UI package in composer.lock:"
if [ -f composer.lock ]; then
  php -r '
    $lock = json_decode=file_get_contents("composer.lock"), true;
    foreach(($lock["packages"] ?? []) + ($lock["packages-dev"] ?? []) as $pkg) {
      if ($pkg["name"] === "neos/neos" || $pkg["name"] === "neos/neos-ui") {
        echo $pkg["name"] . " " . $pkg["version"] . " " . ($pkg["source"]["type"] ?? "") . " " . ($pkg["source"]["url"] ?? "") . " " . ($pkg["source"]["reference"] ?? "") . PHP_EOL;
      }
    }
  '
else
  echo "no composer.lock"
fi

echo
echo "Query package source for helper:"
if [ -f composer.lock ]; then
  python3 - <<'PY'
import json, urllib.request
p=json.loads(open("composer.lock").read())
for sec in ("packages","packages-dev"):
    for c in p.get(sec, []):
        if c["name"] in ("neos/neos","neos/neos-ui"):
            ref=c.get("source",{}).get("reference") or c.get("version","")
            if c["name"]=="neos/neos-ui":
                url=c.get("source",{}).get("url") or "https://api.github.com/repos/neos/neos-ui/git/trees/"+ref
            else:
                url=f"https://raw.githubusercontent.com/neos/neos/{ref}/Neos.Neos/Classes/Fusion/Helper/ContentDimensionsHelper.php"
            print(f"--- {c['name']} {ref}")
            print(url)
            try:
                with urllib.request.urlopen(url, timeout=20) as r:
                    text=r.read().decode("utf-8","replace")
                for i,line in enumerate(text.splitlines(),1):
                    if i<=90:
                        print(f"{i:4}: {line}")
            except Exception as e:
                print(f"FETCH_ERROR: {e}")
PY
fi

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 2496


🌐 Web query:

Neos 9 Neos.Neos Fusion Helper ContentDimensionsHelper source contentRepositoryRegistry

💡 Result:

In Neos 9, the legacy ContentDimensionsHelper has been superseded by the new DimensionHelper (Neos\Neos\Fusion\Helper\DimensionHelper) to handle content dimensions within Fusion [1]. Key points regarding the transition and usage in Neos 9: 1. DimensionHelper: This is the new Eel helper used to interact with content dimensions [1]. You can access it in Fusion via Neos.Dimension [1]. Common methods include: - Neos.Dimension.currentValue(node, dimensionName): Retrieves the current dimension value for a node [2][1]. - Neos.Dimension.originValue(node, dimensionName): Gets the original dimension value, which may differ from the current value if dimension fallbacks are active [2][3]. - Neos.Dimension.all(node/contentRepositoryId): Finds all content dimensions defined for the given node or repository [2][1]. - Neos.Dimension.allDimensionValues(node/contentRepositoryId, dimensionName): Retrieves all dimension values for a specific dimension [2][1]. 2. ContentRepositoryRegistry: While not a Fusion helper, the ContentRepositoryRegistry (Neos\ContentRepositoryRegistry\ContentRepositoryRegistry) is the central entry point in Neos 9 for fetching Content Repository instances in PHP [4][5]. Within the DimensionHelper's implementation, the registry is used to resolve the specific Content Repository associated with a node, allowing the helper to query the appropriate variation graph [3][6]. 3. Upgrade Context: If you are migrating from Neos 8.3 to 9.0, note that the previous approach of accessing dimensions via site.context.dimensions is no longer available because the Request Context has been removed [1]. You must update your Fusion code to use the Neos.Dimension helper instead [1].

Citations:


🌐 Web query:

github.com neos neos-development-collection 9.0 Neos.News.Fusion.Helper.ContentDimensionsHelper.php

💡 Result:

In Neos 9.0, there is no file named ContentDimensionsHelper.php in the Neos.News.Fusion (or Neos.Neos) package [1][2][3][4]. It appears you are likely looking for the Neos.Dimension helper, which was introduced in Neos 9.0 to handle content dimension-related tasks in Fusion [5][6]. The class responsible for this functionality is Neos\Neos\Fusion\Helper\DimensionHelper [1][2]. You can find this helper at the following location in the neos-development-collection repository: Neos.Neos/Classes/Fusion/Helper/DimensionHelper.php [1] This helper provides several methods for working with node dimensions in Fusion, including: - currentValue(Node $node, ContentDimensionId|string $dimensionName): Retrieves the current dimension value for a node [2][5]. - originValue(Node $node, ContentDimensionId|string $dimensionName): Retrieves the original dimension value, differing from the current value in cases of dimension fallback [2][5]. - all(ContentRepositoryId|Node $subject): Finds all content dimensions in a repository [2][5]. - allDimensionValues(ContentRepositoryId|Node $subject, ContentDimensionId|string $dimensionName): Finds all content dimension values [2][5]. - findVariantInDimension(Node $node, ContentDimensionId|string $dimensionName, ContentDimensionValue|string $dimensionValue): Finds the variant of a node in a specified dimension and value [2][5].

Citations:


🌐 Web query:

github.com neos/neos-ui 9.0 Fusion/Helper/ContentDimensionsHelper.php

💡 Result:

The file ContentDimensionsHelper.php, located in the Neos UI package (neos/neos-ui), is an Eel helper class responsible for handling content dimension configurations within the Neos CMS user interface [1][2]. In older versions of Neos (such as 7.1.x), this helper was explicitly documented as Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper and provided Eel methods like ContentDimensions.allowedPresetsByName and ContentDimensions.contentDimensionsByName [2]. In Neos 9.0 and later versions, the core handling of content dimensions and node variants has been significantly refactored alongside the Content Repository (CR) updates [3][4]. Functionality previously handled by various UI-specific helpers has been migrated into the core Neos namespace [5][4]. For instance, current dimension management in Fusion is primarily handled by the Neos\Neos\Fusion\Helper\DimensionHelper [5][4]. If you are working with Neos 9.0, you should look for updated Fusion helpers within the Neos\Neos\Fusion\Helper namespace rather than relying on legacy UI-specific helpers [5][4].

Citations:


🌐 Web query:

siteground Kronospan neos neos-ui 9.0 ContentDimensionsByName ContentDimensionsHelper

💡 Result:

In Neos CMS 9.0, which utilizes the new event-sourced Content Repository, the legacy Eel helpers such as ContentDimensionsHelper and methods like ContentDimensionsByName have been replaced or reorganized [1][2]. For accessing dimension information in Fusion, Neos 9.0 provides the Neos.Dimension helper, which is implemented in the class Neos\Neos\Fusion\Helper\DimensionHelper [3][4]. This helper allows you to interact with content dimensions and their values for a given node or content repository ID [3][4]. Key methods available in the Neos.Dimension (DimensionHelper) include: 1. Neos.Dimension.all(node) or Neos.Dimension.all(contentRepositoryId): Returns all content dimensions defined in the content repository, ordered by priority [3][4]. 2. Neos.Dimension.allDimensionValues(node, dimensionName): Retrieves all content dimension values for a specific dimension [3][4]. 3. Neos.Dimension.currentValue(node, dimensionName): Returns the current dimension value for a specific node [3]. 4. Neos.Dimension.findVariantInDimension(node, dimensionName, dimensionValue): Finds a specific variant of a node within a given dimension and value [3]. The legacy class Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper and its associated Eel methods (like contentDimensionsByName) are associated with older versions of Neos (e.g., 7.3.x) and are not the standard approach in the 9.0 architecture [5]. If you are migrating to or developing on Neos 9.0, you should update your Fusion code to utilize the Neos.Dimension helper [3].

Citations:


🌐 Web query:

raw.githubusercontent.com neos neos-ui 9.0 Neos.Neos.Ui Classes Fusion Helper ContentDimensionsHelper.php

💡 Result:

In Neos 9.0, the class Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper is associated with the Neos UI package, but official documentation and the 9.0 source code indicate that content dimension handling has evolved significantly in this version [1][2]. While documentation for Neos 7.3 references ContentDimensionsHelper to provide methods such as allowedPresetsByName and contentDimensionsByName [3], Neos 9.0 introduces a new content repository architecture (often referred to as the "CR Core") [1]. In the 9.0 API, functionality related to content dimensions has shifted toward the Neos\Neos\Fusion\Helper\DimensionHelper class [4]. DimensionHelper provides several methods for managing nodes across dimensions, including: - all(ContentRepositoryId|Node $subject): Finds all content dimensions in a repository or for a specific node [4]. - currentValue(Node $node, ContentDimensionId|string $dimensionName): Retrieves the current dimension value for a node [4]. - originValue(Node $node, ContentDimensionId|string $dimensionName): Retrieves the original dimension value, accounting for dimension fallbacks [4]. - findVariantInDimension(Node $node, ContentDimensionId|string $dimensionName, ContentDimensionValue|string $dimensionValue): Locates a node variant in a specific dimension [4]. If you are upgrading from older versions, check the Neos 9.0 documentation and the new DimensionHelper class, as direct reliance on ContentDimensionsHelper may be deprecated or replaced by the updated CR infrastructure [1][4].

Citations:


🌐 Web query:

github.com neos/neos-ui master Neos.Neos.Ui Classes Fusion Helper ContentDimensionsHelper.php contentDimensionsByName

💡 Result:

In Neos CMS, the functionality for handling content dimensions in Fusion is primarily provided by the DimensionHelper, which is registered as the Neos.Dimension Eel helper [1]. There is no public method named contentDimensionsByName in the standard Neos.Dimension helper or the associated DimensionHelper.php class [2][3][1]. Instead, to work with content dimensions in Fusion, the Neos.Dimension helper provides the following methods [3][1]: 1. Neos.Dimension.all(subject): Retrieves all content dimensions for a given node or content repository ID [2][3][1]. 2. Neos.Dimension.allDimensionValues(subject, dimensionName): Finds all available values for a specific content dimension [2][3][1]. 3. Neos.Dimension.currentValue(node, dimensionName): Gets the current dimension value for a specific node [3][1]. 4. Neos.Dimension.originValue(node, dimensionName): Gets the original (non-fallback) dimension value for a node [3][1]. 5. Neos.Dimension.findVariantInDimension(node, dimensionName, dimensionValue): Finds a specific variant of a node within a given dimension and value [3][1]. If you are looking to retrieve a specific dimension definition by its name (ID), you would typically interact with the ContentDimensionSource, which is accessible via the underlying Neos Content Repository API rather than a direct Fusion helper method [4].

Citations:


Replace the legacy Neos UI content dimensions helper.

This package targets Neos 9, where Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper::contentDimensionsByName() is no longer the supported API; use Neos\Neos\Fusion\Helper\DimensionHelper / the Neos.Dimension Eel helper equivalents instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/EelHelper/NEOSidekickInternalHelper.php` around lines 100 - 104,
Update contentDimensionsByName() to stop instantiating the legacy
Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper and retrieve the dimensions
through the supported Neos\Neos\Fusion\Helper\DimensionHelper or equivalent
Neos.Dimension Eel helper API, preserving the existing content repository
context and array result.

Comment thread Classes/Service/NodePatchService.php
Comment on lines +357 to +375
if ($hidden === true) {
$contentRepository->handle(DisableNodeAggregate::create(
$subgraph->getWorkspaceName(),
$newNodeAggregateId,
$subgraph->getDimensionSpacePoint(),
NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS
));
}

// The projection is updated synchronously, so the new node is queryable right away
$newNode = $subgraph->findNodeById($newNodeAggregateId);
if ($newNode === null) {
throw new PatchFailedException(
sprintf('Node "%s" was created but cannot be found in the subgraph', $newNodeAggregateId->value),
$index,
'createNode',
$patch->getPositionRelativeToNodeId()
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Read the created node before disabling it.

If the resolved subgraph applies any visibility constraint, findNodeById() after DisableNodeAggregate returns null and every hidden-node creation fails with "created but cannot be found" after the node has already been created (no rollback in the event-sourced CR). Reordering removes the dependency on the subgraph's visibility semantics entirely.

🐛 Proposed fix
-            if ($hidden === true) {
-                $contentRepository->handle(DisableNodeAggregate::create(
-                    $subgraph->getWorkspaceName(),
-                    $newNodeAggregateId,
-                    $subgraph->getDimensionSpacePoint(),
-                    NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS
-                ));
-            }
-
             // The projection is updated synchronously, so the new node is queryable right away
             $newNode = $subgraph->findNodeById($newNodeAggregateId);
             if ($newNode === null) {
                 throw new PatchFailedException(
                     sprintf('Node "%s" was created but cannot be found in the subgraph', $newNodeAggregateId->value),
                     $index,
                     'createNode',
                     $patch->getPositionRelativeToNodeId()
                 );
             }
+
+            if ($hidden === true) {
+                $contentRepository->handle(DisableNodeAggregate::create(
+                    $subgraph->getWorkspaceName(),
+                    $newNodeAggregateId,
+                    $subgraph->getDimensionSpacePoint(),
+                    NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS
+                ));
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($hidden === true) {
$contentRepository->handle(DisableNodeAggregate::create(
$subgraph->getWorkspaceName(),
$newNodeAggregateId,
$subgraph->getDimensionSpacePoint(),
NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS
));
}
// The projection is updated synchronously, so the new node is queryable right away
$newNode = $subgraph->findNodeById($newNodeAggregateId);
if ($newNode === null) {
throw new PatchFailedException(
sprintf('Node "%s" was created but cannot be found in the subgraph', $newNodeAggregateId->value),
$index,
'createNode',
$patch->getPositionRelativeToNodeId()
);
}
// The projection is updated synchronously, so the new node is queryable right away
$newNode = $subgraph->findNodeById($newNodeAggregateId);
if ($newNode === null) {
throw new PatchFailedException(
sprintf('Node "%s" was created but cannot be found in the subgraph', $newNodeAggregateId->value),
$index,
'createNode',
$patch->getPositionRelativeToNodeId()
);
}
if ($hidden === true) {
$contentRepository->handle(DisableNodeAggregate::create(
$subgraph->getWorkspaceName(),
$newNodeAggregateId,
$subgraph->getDimensionSpacePoint(),
NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS
));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodePatchService.php` around lines 357 - 375, Reorder the
hidden-node creation flow so `$subgraph->findNodeById($newNodeAggregateId)` and
its null check occur before dispatching `DisableNodeAggregate`. Keep the
existing disable operation afterward, ensuring hidden nodes are validated while
visible and avoiding dependence on subgraph visibility constraints.

Comment on lines +36 to +39
// Remember which asset is listed first (persistence check below is identity-based:
// page counts are unreliable because the module refills pages from the asset pool)
const firstImageSrc = await page.locator('#appContainer img').first().getAttribute('src');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the test file and relevant test block.
fd -a 'neos9.bulkAltTextModule.spec.ts' . | sed 's#^\./##'
file="$(fd 'neos9.bulkAltTextModule.spec.ts' . | head -n1)"
echo "FILE=$file"
wc -l "$file"
echo "==== lines 1-120 ===="
sed -n '1,120p' "$file" | cat -n

echo "==== occurrences of firstImageSrc and related assertions ===="
rg -n "firstImageSrc|asset|title|persisted|isVisible|locator\\('`#appContainer` img'\\)|`#appContainer` img|first()" "$file"

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 7145


🌐 Web query:

Playwright Locator click button Start generation Generierung starten waitForTest 106 documentation

💡 Result:

In Playwright, there is no built-in method named waitForTest. It is likely a misunderstanding of testInfo or a custom helper function used in specific test suites [1][2]. To interact with a button labeled "Start generation" (or "Generierung starten"), use Playwright's locator API, which is designed to handle synchronization automatically [3][4]. You should use getByRole to target the button [5]. Recommended implementation: // TypeScript/JavaScript await page.getByRole('button', { name: 'Start generation' }).click; // Or for German locale: await page.getByRole('button', { name: 'Generierung starten' }).click; Key concepts for this task: 1. Locator Auto-waiting: You do not need to manually wait for the button to appear [3]. The click() action automatically performs actionability checks—including waiting for the element to be visible, stable (not animating), enabled, and receiving pointer events—before executing the click [6][4]. 2. Test Generator (Codegen): If you are struggling to identify the correct selector, you can use Playwright's built-in test generator by running npx playwright codegen <your-url>. This tool allows you to interact with the page and automatically generates the recommended locators, prioritizing robust options like getByRole [7][8]. 3. Handling Timeouts: If your test times out, it is often because the element is not meeting actionability criteria (e.g., it is obscured or disabled). You can verify the element's state or, in exceptional cases, use the force: true option to bypass these checks: await page.getByRole('button', { name: 'Start generation' }).click({ force: true }); [6][4] If you encountered waitForTest in a codebase, please check if it is defined as a custom utility function in your project, as it is not part of the official Playwright API [1]. If you are looking to manage test retries or information, refer to the testInfo object instead [1][2].

Citations:


🌐 Web query:

Playwright Locator isVisible return boolean timeout option documented

💡 Result:

The Playwright locator.isVisible method returns a boolean value (true if the element is visible, false otherwise) [1][2][3]. While some language bindings may still include a timeout option in their API signature for backward compatibility, it is officially deprecated [2][4][5]. The timeout option is ignored because locator.isVisible is designed to perform an immediate check without waiting for the element to become visible [1][2][5][6]. If you need to wait for an element to become visible, you should use the Web-First assertion expect(locator).toBeVisible instead, which includes built-in retry logic and configurable timeouts [2][3][6].

Citations:


Assert the saved asset by identity and wait for the post-save state.

isVisible({ timeout: 60_000 }) returns immediately, so this can compare against state before the post-save list has rendered. The first image source also doesn’t identify the saved asset; ordering or pagination changes can make another asset appear first. Capture a stable asset/key identifier, wait for the list empty state or the post-save list via an explicit assertion, then assert that specific asset no longer appears or its persisted title is present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts` around lines 36 - 39, Update
the persistence check around firstImageSrc to capture a stable identifier for
the asset being edited instead of relying on the first image source. After
saving, explicitly wait for the post-save list state using an assertion for the
empty state or rendered list, then verify that specific asset is absent or its
persisted title is present; do not use immediate isVisible checks or list
ordering as the synchronization/identity mechanism.

Source: MCP tools

Comment on lines +47 to +55
// Step 4: save (persists via the updateAssets endpoint)
const saveAll = page.getByRole('button', { name: /Save all and get next page|Alle speichern/i }).first();
await expect(saveAll).toBeVisible({ timeout: 30_000 });
await saveAll.click();
console.log('[e2e] save clicked');

// Saving either advances to the next page or shows the saved state; there must be no alert
await page.waitForTimeout(5_000);
await expect(page.locator('#appContainer [role="alert"], #appContainer .neos-error')).toHaveCount(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate target file =="
fd -a 'neos9.bulkAltTextModule.spec.ts' . || true

echo "== inspect target file =="
target="$(fd 'neos9.bulkAltTextModule.spec.ts' . | head -n 1)"
if [ -n "${target:-}" ]; then
  wc -l "$target"
  sed -n '1,120p' "$target" | cat -n
fi

echo "== search for updateAssets / save flow =="
rg -n "updateAssets|Save all|Alle speichern|waitForTimeout|afterPageReload|reload|toBeVisible\\(.*timeout" . \
  -g '!node_modules' -g '!dist' -g '!build' || true

echo "== package playwright versions =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
  [ -f "$f" ] && { echo "--- $f"; case "$f" in package.json) jq -r '.dependencies, .devDependencies' "$f" | rg -n "playwright|`@playwright/test`" || true;; *) rg -n "`@playwright/test`|playwright" "$f" | head -n 40 || true;; esac; }
done

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 50392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== list e2e tests under Tests/E2E/tests =="
find Tests/E2E/tests -maxdepth 1 -type f -name '*.spec.ts' 2>/dev/null | sort | head -n 80

echo "== inspect target file with wc if exists =="
target="Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts"
if [ -f "$target" ]; then
  wc -l "$target"
  sed -n '1,130p' "$target" | cat -n
else
  echo "target file not found"
fi

echo "== focused rg for relevant implementation/API terms =="
rg -n "updateAssets|saveAll|Save all|Alle speichern|waitForTimeout|after reload|Reload|reload|waitForResponse|response|status" \
  Tests/E2E src . \
  -g '!node_modules' -g '!dist' -g '!build' -g '!coverage' --max-count 120 || true

echo "== package manifests for playwright and repo root =="
find . -maxdepth 2 -type f \( -name 'package.json' -o -name 'pnpm-lock.yaml' -o -name 'package-lock.json' -o -name 'yarn.lock' \) -print | sort | while read -r f; do
  echo "--- $f"
  rg -n "playwright|`@playwright/test`" "$f" | head -n 40 || true
done

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 50392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== focused file discovery =="
git ls-files | rg '(^|/)neos9\.bulkAltTextModule\.spec\.ts$|package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$' || true

echo "== target spec relevant lines if present =="
target="Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts"
if [ -f "$target" ]; then
  sed -n '1,140p' "$target" | cat -n
  echo "== updateAssets usages in target =="
  rg -n "updateAssets|waitForResponse|response\\.status\\(|toBeVisible|toHaveCount|timeout" "$target" || true
else
  echo "target missing"
fi

echo "== focused updateAssets implementation search =="
rg -n "updateAssets|update.*asset|asset.*save|saveAll|after.*reload|reload.*asset|waitForResponse" \
  Tests/E2E src . \
  -g '!node_modules' -g '!dist' -g '!build' -g '!coverage' --max-count 160 || true

echo "== package playwright declarations =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n "playwright|`@playwright/test`" "$f" | head -n 80 || true
  fi
done

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 50392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository top-level =="
pwd
git rev-parse --show-toplevel
git ls-files | head -n 120

echo "== target spec exact =="
if [ -f Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts ]; then
  wc -l Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts
  sed -n '1,140p' Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts | cat -n
else
  echo "missing Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts"
fi

echo "== exact updateAssets matches in tracked files =="
git lfs ls-files 2>/dev/null || true
git ls-files | xargs rg -n "updateAssets" || true

echo "== exact E2E save/wait references in tracked files =="
rg -n "waitForTimeout\\(5_000\\)|afterPageReload|after reload|afterReload|waitForResponse|response\\.status\\(.*updateAssets|persistItems|Save all and get next page|Alle speichern" \
  Tests/E2E src . \
  --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' --glob '!*.lock' || true

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 50392


Wait for save completion instead of sleeping for five seconds.

waitForTimeout(5_000) can expire before the save completes, so the subsequent reload may observe stale state and make this test flaky. Replace the fixed delay with a reaction to the save response/confirmation or the next-page/saved-state UI before asserting errors and reloading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/E2E/tests/neos9.bulkAltTextModule.spec.ts` around lines 47 - 55,
Replace the fixed page.waitForTimeout(5_000) after saveAll.click() with an
explicit wait for the save completion signal, such as the updateAssets response,
confirmation UI, next-page navigation, or saved-state UI. Keep the existing
no-alert assertion and ensure they run only after the save has completed.

Source: MCP tools

Comment on lines +74 to +104
if (await continueBtn.isVisible({ timeout: 10_000 }).catch(() => false)) {
console.log('[e2e] clicking interstitial');
await continueBtn.click();
}

const authorizeBtn = popup.getByRole('button', { name: /^Authorize$/i });
await expect(authorizeBtn).toBeVisible({ timeout: 15_000 });
await authorizeBtn.click();
console.log('[e2e] authorize clicked');

// The popup shows "Authorization completed" and may close itself once the chat
// picks up the token — don't fail if it's already gone.
const completed = await popup
.getByText(/Authorization completed/i)
.isVisible({ timeout: 15_000 })
.catch(() => false);
console.log('[e2e] authorization completed page shown:', completed);
await popup.close().catch(() => undefined);
}

// The chat should now render its input (it reloads itself after the popup completes;
// fall back to one page reload if it doesn't react within 30s).
const chatInput = () => page
.frameLocator('iframe[src*="api.neosidekick"]')
.locator('textarea, [contenteditable="true"], input[type="text"]')
.first();
const ready = await chatInput().isVisible({ timeout: 30_000 }).catch(() => false);
console.log('[e2e] chat ready without reload:', ready);
if (!ready) {
await page.reload();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Playwright locator.isVisible timeout option ignored non-retrying

💡 Result:

In Playwright, the timeout option in locator.isVisible() is ignored because the method is designed as a "one-shot" operation that returns immediately [1][2][3]. It does not perform any waiting or retrying to determine the visibility of an element [2][4][3]. Key technical points regarding this behavior: - No Retrying: Unlike Playwright actions (e.g., .click()) or web-first assertions (e.g., expect(locator).toBeVisible()), isVisible() does not wait for the element to appear in the DOM or become visible [4][3][5]. It simply checks the current state at the exact moment the call is made [1][2]. - Deprecated Option: The timeout option in isVisible() has been deprecated for a significant period [6][7]. Recent updates have moved to prevent its use by typing it as never in TypeScript definitions to signal that it is a no-op [1][6]. - Recommended Alternatives: - For asserting visibility with a timeout (which includes automatic retries), use the web-first assertion: await expect(locator).toBeVisible({ timeout: 5000 }); [8][5]. - If you need to wait for an element to appear without asserting/failing the test, use await locator.waitFor({ state: 'visible', timeout: 5000 }); [9]. Note that if the element does not appear within the specified timeout, waitFor() will throw an error, whereas isVisible() would simply return false [9]. Developers are encouraged to use these alternatives to avoid flaky tests caused by conditional logic that relies on immediate state checks [4][3][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate package files =="
fd -a 'package\.json$' . | sed 's#^\./##' || true

echo "== Playwright versions =="
for f in $(fd 'package\.json$' .); do
  echo "-- $f --"
  jq -r '(.dependencies.playwright?, .devDependencies.playwright?, .dependencies["`@playwright/test`"]?, .devDependencies["`@playwright/test`"]?) // empty' "$f" || true
done

echo "== target snippet context =="
if [ -f "Tests/E2E/tests/neos9.editorsAndChat.spec.ts" ]; then
  nl -ba "Tests/E2E/tests/neos9.editorsAndChat.spec.ts" | sed -n '60,110p'
else
  echo "target file not found"
fi

echo "== isVisible timeout usages =="
rg -n "isVisible\(\s*\{\s*timeout|getByText\(.*\.isVisible\(\{ timeout|chatInput\(\)\.isVisible\(\{ timeout" Tests/E2E/tests/neos9.editorsAndChat.spec.ts .

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 619


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file line count and relevant section =="
if [ -f "Tests/E2E/tests/neos9.editorsAndChat.spec.ts" ]; then
  awk '{printf "%6d %s\n", NR, $0}' "Tests/E2E/tests/neos9.editorsAndChat.spec.ts" | sed -n '60,110p'
else
  echo "target file not found"
fi

echo "== isVisible timeout usages in target =="
grep -n "isVisible" "Tests/E2E/tests/neos9.editorsAndChat.spec.ts" || true

echo "== Playwright timeout type evidence from installed/type definitions if present =="
f="Tests/E2E/package.json"
if [ -f "$f" ]; then
  sed -n '1,120p' "$f"
fi

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 3705


Replace isVisible() with retry-based waits.

locator.isVisible({ timeout }) is a one-shot visibility check and does not wait for the element to appear, so the chat-readiness gate rejects before the 30s timeout and makes the page.reload() fallback fire much more often than needed. Use waitFor({ state: 'visible', timeout: 30_000 }) or expect(...).toBeVisible(...) with a catch wrapper instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/E2E/tests/neos9.editorsAndChat.spec.ts` around lines 74 - 104, The
chat-readiness check using chatInput().isVisible({ timeout: 30_000 }) is a
one-shot check and should use a retry-based wait instead. Update the chat input
readiness flow to call waitFor({ state: 'visible', timeout: 30_000 }) or
expect(...).toBeVisible(...) with the existing catch-to-false behavior, while
preserving the page.reload() fallback when the wait fails.

- neos/neos, neos/media, neos/neos-ui: ~8.3 -> ^9.0
- neos/seo: * -> ^4.2 (first Neos 9 release)
- flowpack/nodetemplates: ^2.x -> ^3.0 (Neos 9 release)
- sitegeist/fusionlinkprototypes removed: no Neos 9 support upstream;
  core Neos.Fusion:ActionUri replaces it (Fusion adjusted in a later commit)
bin/rector with NeosRectorSets::CONTENTREPOSITORY_9_0 on Classes/ and Tests/.
22 files changed, purely automated output, no manual edits.
65 'TODO 9.0 migration' comments left by rector mark spots needing manual
rewrite (LegacyContextStub usages, removed NodeData/Context APIs, commands
instead of setters).
…d config

Rewrites everything the automated rector pass could not handle:

- Node queries: NodeData/Doctrine query builders replaced with ContentGraph
  subgraph queries (findDescendantNodes/findChildNodes/findClosestNode) across
  all dimension space points; workspace-chain reduction dropped (subgraphs
  already materialize base-workspace content); hidden checks via subtree tags.
- Node identity towards the JS frontend: legacy context paths replaced with
  NodeAddress JSON (opaque, round-tripped); services accept both NodeAddress
  JSON and plain aggregate ids.
- Mutations: SetNodeProperties / SetNodeReferences / CreateNodeAggregateWithNode
  (incl. Flowpack.NodeTemplates 3.0 handler) / Move/Remove/Disable/Enable
  commands. Patch application is no longer transactional (event-sourced CR has
  no rollback); all patches are pre-validated instead, dryRun stops after
  validation.
- URI building: LinkingService replaced with NodeUriBuilder(+Factory); public
  URI -> node resolution via EventSourcedFrontendNodeRoutePartHandler with
  SiteDetectionResult route parameters.
- Dimensions: removed Neos.ContentRepository.contentDimensions config reads;
  ContentDimensionSource everywhere; uriSegment mapping now read from the
  site's dimension resolver options.
- PreviewRenderController: site/context rebuilding removed (FusionView resolves
  site itself); NodeTypeManager now per content repository; Core NodeType API
  (->name->value, nullable getNodeType()).
- AssetService: Flow 9 removed Repository::iterate() -> Doctrine toIterable().
- Fusion: Sitegeist.FusionLinkPrototypes replaced with Neos.Fusion:ActionUri on
  the main request (ModuleUri equivalent verified against Neos 9 MenuHelper);
  Neos.Ui.Workspace.getPersonalWorkspace(contentRepositoryId) with
  allowedTargetWorkspaces from its return value.
- frontendConfiguration contentDimensions now served by our own Eel helper
  (core helper requires a ContentRepositoryId not available in that context).

Single-CR assumption ('default') kept with rector-style TODOs. Semantic
differences are flagged inline as 'TODO 9.0 migration (manual)'.

Unit tests: 19/19 green. flow core:compile clean on Neos 9.1.6.
…Fusion

Neos 9 registers the UI helpers only in Neos.Neos.Ui.configurationDefaultEelContext,
no longer in Neos.Fusion.defaultContext, so getPersonalWorkspace() silently evaluated
to null in our module Fusion and Array.set() fataled. Also guards the workspaces map
against a missing personal workspace.
…ation)

Runs against the Neos.Demo-based Neos 9 project (English backend UI):
- focus keyword: 'Calculate with Sidekick' produces real-API suggestions,
  applying one fills the editor input
- sidebar chat: consent popup flow (interstitial -> Authorize -> completed)
  and chat becomes ready; tolerates an already-authorized state
Docks the sidebar via localStorage init script: in fresh headless profiles the
fullscreen state would overlay the inspector.
…ibility helper

VisibilityConstraints::default()/withoutRestrictions() are deprecated since 9.0
beta 19 and will be removed in Neos 10. New NodeVisibility helper maps intent:
excludeDisabledAndRemoved() = old default context, excludeRemoved() = old
invisibleContentShown context; built on NeosVisibilityConstraints.
…sitory

FunctionalTestCase rewrite:
- CR setup via ContentRepositoryMaintainer per test (Flow's testable persistence
  drops the non-ORM cr_* tables when compiling the test schema) + prune for
  isolation; live workspace, Neos.Neos:Sites root and site nodes via commands.
- Content creation via CreateNodeAggregateWithNode/SetNodeProperties/
  CreateNodeVariant/TagSubtree/RemoveNodeAggregate/PublishWorkspace helpers.
- Template methods setUpContentInLive()/setUpContentInUserWorkspace(): user
  workspaces fork their base content stream at creation, so live content must
  exist before the workspaces are created (unlike Neos 8 context views).
- Assertions on NodeAddress JSON keys (addressForPath) instead of context paths.
- Mock ControllerContexts now carry a SiteDetectionResult route parameter
  (required by NodeUriBuilder in Neos 9).
- Test languages are the distribution's dimension values de/en_US: Neos 9
  validates the URI segment mapping against the dimension source and Flow's
  config merge cannot cleanly replace the distribution's mapping list.
- Testing:HomePage is now a Neos.Neos:Site subtype (children of Neos.Neos:Sites
  must be sites in Neos 9).
- AssetUsage + ImpendingHardRemovalConflictDetection catch-up hooks disabled in
  Testing (their non-ORM tables are dropped by the test schema compile).

Semantic change (documented in NodeServiceWorkspacesTest): nodes disabled in a
user workspace are now correctly excluded from that workspace's results and,
after publishing, from live - the Neos 8 suite documented the old SQL-based
behavior as a quirk.

Result: 23 tests, 255 assertions, all green (plus 19 unit tests).
Configure -> auto-generation via real API -> save-all -> persistence verified
(saved asset drops off the unset-only listing). Exercises findAssets,
updateAssets and the Flow 9 AssetService iteration fix end-to-end.
Requires a publicly reachable PLAYWRIGHT_BASE_URL (ddev share) so the
NEOSidekick API can fetch the images.
…n notes

- New ContentRepositoryProvider (singleton) resolves the content repository from
  the new NEOSidekick.AiAssistant.contentRepositoryId setting (default
  'default'); all 14 inline registry lookups now go through it, so a future
  multi-CR implementation (e.g. via SiteDetectionResult) is a one-place change.
- All remaining 'TODO 9.0 migration' comments resolved: the multiple-CR ones by
  the provider, the '(manual)' ones reworded to 'NOTE (Neos 9 migration
  decision)' - they document deliberate, permanent semantic differences
  (result ordering, dimension fallback collapsing, non-transactional patch
  application, origin-DSP property writes), not open work.

Verified: 19 unit + 23 functional + 4 Playwright E2E tests green.
Generality review results:

- Functional tests no longer assume the Neos.Demo dimension configuration
  (de/en_US constants, /de/ URL literals): the test base now derives the two
  test languages and their URI segments from the running content repository and
  site configuration (primaryLanguage() prefers a non-site-default value so
  test URLs keep their segment prefix). Dimension-variant tests skip themselves
  on distributions with fewer than two top-level language values; the suffix
  helper reads the site's uriPathSuffix.
- E2E specs renamed (neos9.editorsAndChat / neos9.bulkAltTextModule): they run
  on any Neos 9 site with the plugin's default configuration, not only
  Neos.Demo. Docblocks now state the exact requirements (API key, English
  backend UI, public base URL for the bulk module) and warn that they WRITE to
  the instance. The bulk module persistence assertion is now identity-based
  (first-listed asset must change) instead of page-count-based, which broke on
  repeated runs.
- README: Neos compatibility matrix, the contentRepositoryId setting, and the
  LostInTranslation dimension-options relocation documented; TESTING.md
  documents the MariaDB/MySQL test-database requirement (the content graph
  does not support the SQLite Testing default) and the dimension adaptivity;
  Tests/E2E/README.md documents the Neos 9 specs.

Production code was audited for project-specific assumptions: none found (the
'language' occurrences are DTO field names / API payload keys; dimension and
CR access are driven by the languageDimensionName / contentRepositoryId
settings). The Neos.Demo:* node type overrides under Configuration/Testing
pre-date this migration and only apply in the Testing context.

Verified: 19 unit + 23 functional + 4 E2E green after the refactor.
The Neos 9 content graph requires MariaDB/MySQL, so the functional tests can
no longer run on the base distribution's SQLite Testing default. Following the
setup of neos/neos-development-collection's build.yml:

- matrix targets Neos 9.0 + 9.1 only (this branch is a clean Neos 9 fork,
  main keeps the 8.x pipeline); temporary push trigger for the feature branch
  so the pipeline itself can be tested before merge
- mariadb:10.11 service container (health-checked, db flow_functional_testing)
- new step overwrites the base distribution's Testing Settings.yaml with the
  service connection (mirror of core's configure-neos composite action); the
  plugin's own Configuration/Testing contributes the rest
- pdo_sqlite extension dropped

Also fixes 5 auto-fixable PSR-12 violations introduced by the migration
(multi-line control structures) found while pre-verifying the phpcs step
locally; phpstan (CI configuration) verified clean, functional suite re-run
green after the fixes.
UserService::getPersonalWorkspaceName() still exists in Neos 9 but throws
(neos/neos-development-collection#5418) - it survived the migration unnoticed
because Flow proxy compilation and the test suites never call it: only the
JWT-authenticated agent API endpoints do, at runtime. All five callers
(SearchNodes, DocumentNodeList, GetPreview, ApplyPatches, NodeTreeSchema)
returned 500 for every chat agent action that fetches page content.

New PersonalWorkspaceService resolves the current user's personal workspace
through Neos\Neos\Domain\Service\WorkspaceService metadata and preserves the
old null contract (no user / no workspace assigned -> null), so the
requested-workspace fallbacks in the controllers behave as before. The legacy
Neos\Neos\Service\UserService injections became unused and were removed.

Verified: 19 unit + 23 functional green, phpcs/phpstan clean; unauthenticated
endpoint returns 401 (auth layer unaffected).
…l generate waits

- @neos-project/neos-ui-extensibility + react-ui-components ^8.3 -> ^9.1.6,
  @fortawesome/free-* ^5 -> ^6.5.2 (9.x peer requirement); Plugin.js/.css and
  BackendModule.js rebuilt from the updated stack (react stays 16-typed per the
  9.1.6 peer range - the host UI provides React at runtime via the
  extensibility alias map).
- .npmrc pins the @FortAwesome scope to the public registry: the plugin uses
  the free icon packages and must not resolve them through a Pro registry
  configured in a developer's user-level npm config (matches the historical
  yarn.lock resolution).

Robustness: an image alt-text generate button was observed stuck in an endless
loading state (not deterministically reproducible; generation itself verified
working repeatedly). Every await in the generate chain that could pend forever
without an error is now bounded and reports a flash message instead:
- ContentService.getGuestFrameContentDocument(): the unbounded 100ms poll
  loops now fail after 15s (previously spun forever on an unreachable or
  never-finishing guest frame, e.g. for documentContent-based generations)
- ApiService.fetch(): 120s AbortSignal timeout (stalled tunnel connections)
- ContentService.getImageMetadata(): 30s race around the host UI's
  loadImageMetadata endpoint

Verified on the rebuilt bundles: all 4 Playwright E2E specs green, alt-text
generation on Neos.Demo produces and commits a description.
…ge URIs

The important-pages filter (focus keyword, SEO title & meta description, SEO
image alt-text backend modules) sent one '<host>/<uriSegment>' URL per language
to the NEOSidekick API. In Neos 9 the site-default language's homepage has an
EMPTY uri path ('/en' 404s while '/' works), so the API failed to crawl the
first URL and reported the whole site as not publicly accessible.

The entry URL per language is now the router-generated homepage URI of the
site node in that language's dimension space point (NodeUriBuilder, live
workspace); languages without a resolvable homepage variant are skipped. This
also removes the manual uriSegment-mapping extraction - the router is the
single source of truth for any dimension resolver configuration.

Verified on the demo instance through a public tunnel with the real API: all
three important-pages modules now list pages (previously: 'website must be
publicly accessible'). Functional suite green (23/255) including the strict
host-list mock, unit 19/19, phpcs/phpstan clean, 4 E2E specs green.
… remain

The spec's documented precondition (at least one image asset without a title)
consumes itself: every run persists titles. Repeated runs on the same instance
now skip with an explanatory message instead of failing.
…on save

Symptom (SEO title & meta description module): important-pages and custom
filter modes appeared inconsistent — custom listed mostly English (US) while
important-pages listed English (UK) and German rows.

The enumeration difference is correct and matches Neos 8: the custom filter
lists one row per EXISTING origin variant (the demo has 46 en_US but only 2
de / 2 en_UK variants), while important-pages lists one row per public URL —
including fallback URLs like /uk/... serving en_US content, which the crawler
legitimately reports as distinct pages.

Underneath sat three real dimension-semantics bugs for such fallback rows
(address dimension != origin dimension), all fixed to use the SERVED dimension
like the old CR context dimensions:

- FindDocumentNodeDataFactory labeled fallback rows with their ORIGIN language
  (every /uk/... row showed 'English (US)').
- The language dimension filter matched the origin, so filtering by en_UK
  would have dropped all fallback UK rows.
- WORST: updatePropertiesOnNodes wrote to the node's origin — saving an SEO
  title on a /uk/... row would silently overwrite the /en/... value. Now the
  variant is materialized first (CreateNodeVariant, copy-on-write like Neos 8
  contexts) and the properties are written to the addressed dimension.

Verified on the demo instance: all 24 important-pages rows label/dimension
consistent (10x en_UK, 13x en_US, 1x de); writing focusKeyword to a /uk
fallback row created an en_UK origin variant carrying the value while the
en_US origin remained untouched (checked in the content graph). Functional
23/255, unit 19/41, phpcs/phpstan clean.
…licit variants

Design decision (supersedes the copy-on-write behavior introduced in 9d5156a,
and deliberately diverges from Neos 8, which materialized variants implicitly
via context adoption on setProperty):

Fallback pages (a specialization serving its generalization's content, e.g.
en_UK falling back to en_US) are not editable rows. Materializing a variant as
a save side effect copies the ENTIRE property set and permanently detaches the
page from its fallback chain - future origin edits no longer propagate. That
decision belongs to an editor in the Neos UI ('create variant'), never to a
bulk tool or an agent patch.

- NodeService::findImportantPages(): fallback URLs are re-addressed to their
  ORIGIN dimension and deduped with the origin's own URL row. The underlying
  page stays in the list once, correctly labeled; editing it improves every
  URL it shines through to. Real variants keep their own dimension.
- NodeService::updatePropertiesOnNodes(): writes to fallback addresses are
  rejected (1752060000001) with a message pointing to explicit variant
  creation. find()/findImportantPages() only emit origin-addressed rows, so
  this guards against stale or handcrafted addresses.
- NodePatchService (chat agent updateNode): same rejection as PatchFailed.
- Both backend module modes now consistently enumerate editable origin
  variants (previously important-pages listed each page once per public URL).

New NodeServiceFallbackDimensionsTest (4 tests) covers: fallback URL re-
addressing, real variants keeping their dimension, fallback write rejection,
variant writes leaving the generalization untouched. Skips on distributions
without a language specialization pair. Test base: createPageWithImageNodes()
accepts an optional language.

Verified live: important-pages went from 24 rows (10 fallback duplicates) to
16 origin rows (13 en_US, 1 de, 2 real en_UK variants). Functional 27/282,
unit 19/41, phpcs/phpstan clean.
…dles)

Post-rebase reconciliation after rebasing onto main (d71aa5e + the Neos 8
fallback backport 7bf846b):

- Classes/Service/DimensionFallbackDetector.php (from the Neos 8 backport)
  removed: it is written against the old CR API (NodeInterface/getNodeData);
  on Neos 9 the fallback concept is implemented inline via origin vs. subgraph
  dimension space point comparison (NodeService, NodePatchService) and covered
  by NodeServiceFallbackDimensionsTest.
- Plugin.js/.css rebuilt from the merged sources: the rebase brought main's
  frontend changes (AiModal link styling + loading indicator, CKEditor css,
  ContentCanvasService page-updated handling) which are now compiled into the
  Neos 9 bundle; before the rebuild the published bundle mixed both sides.

Main's other incoming features verified present and Neos-9-compatible:
BaseUriProvider-based domain resolution behind reverse proxies
(NEOSidekickInternalHelper), per-domain website configuration via siteDomain
(ConfigurationController + Root.fusion), and the new
ConfigurationControllerSiteDomainTest (pure mock unit test, runs unchanged).

Verified: 21 unit / 27 functional / phpcs / phpstan green, backend healthy.
Both 9.1 matrix jobs started failing in the 'Install Neos Project and other
dependencies' step on 27.07. (run 30249483831) while the ~9.0.0 jobs kept
passing - immediately after the 9.1.7/9.1.8 releases (15.07.). The composer
sequence with ~9.1.0 (resolving 9.1.8) is not reproducible as broken locally,
so this pins the 9.1 entry to 9.1.6, the release this migration was developed
against and the last one verified green in CI (run 29006708375). Revisit to
unpin once the newer patch releases are investigated on a real runner.
The neos9.editorsAndChat and neos9.bulkAltTextModule specs were written to
verify the migration against the local Neos.Demo instance. They need too much
scaffolding to run anywhere else (API key, publicly reachable instance,
untitled assets, English backend) and write to the instance they run against,
so they do not belong in the repository. The pre-existing E2E suite for the
NEOSidekickTestWebsite stays untouched.
@gradinarufelix gradinarufelix changed the title WIP Neos 9 Support Neos 9 compatibility (event-sourced Content Repository) — new 4.x line Aug 4, 2026
@gradinarufelix
gradinarufelix force-pushed the feature/neos9-compatibility branch from e522e3b to 76be352 Compare August 4, 2026 16:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Classes/Service/DocumentNodeListExtractor.php (1)

150-163: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Use a one-shot descendant query for document extraction.

traverseDocuments() issues findChildNodes() for every visited node, so the default depth: -1 can traverse the whole site subtree. This also counts all children in currentDepth, so non-document wrapping nodes can remove reachable documents from a bounded depth request and change the depth field. Call findDescendantNodes() with FindDescendantNodesFilter::create(nodeTypes: $nodeTypeFilter), then compute document-specific depth if the consumer needs it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/DocumentNodeListExtractor.php` around lines 150 - 163,
Replace the recursive child traversal in traverseDocuments() with a single
findDescendantNodes() query using FindDescendantNodesFilter::create(nodeTypes:
$nodeTypeFilter). Remove the per-node recursion and ensure returned documents
are extracted directly, computing depth based on document-relevant ancestry
rather than all wrapping nodes if the depth field is required.
🧹 Nitpick comments (5)
Classes/Service/DocumentNodeListExtractor.php (1)

115-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused $contentRepository parameter.

resolveSiteNode() does not use $contentRepository. PHPMD flags it. Drop the parameter and the argument at line 89.

♻️ Proposed change
-        $siteNode = $this->resolveSiteNode($contentRepository, $subgraph, $siteNodeName);
+        $siteNode = $this->resolveSiteNode($subgraph, $siteNodeName);
-    private function resolveSiteNode(ContentRepository $contentRepository, ContentSubgraphInterface $subgraph, ?string $siteNodeName): ?Node
+    private function resolveSiteNode(ContentSubgraphInterface $subgraph, ?string $siteNodeName): ?Node
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/DocumentNodeListExtractor.php` at line 115, Remove the unused
$contentRepository parameter from DocumentNodeListExtractor::resolveSiteNode(),
and update its call site around line 89 to stop passing that argument while
preserving the remaining parameters and behavior.

Source: Linters/SAST tools

Tests/Functional/FunctionalTestCase.php (2)

364-380: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixture helpers do not derive the target workspace from the node they act on. Both helpers pick the workspace independently of the passed Node, so a node read from a user-workspace subgraph can be written to live without an error.

  • Tests/Functional/FunctionalTestCase.php#L364-L380: replace the hardcoded WorkspaceName::forLive() and subgraph('live', $language) with $parentNode->workspaceName.
  • Tests/Functional/FunctionalTestCase.php#L399-L407: default $workspace to $node->workspaceName instead of 'live'.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Functional/FunctionalTestCase.php` around lines 364 - 380, The fixture
helpers must use the workspace associated with their input node. In
Tests/Functional/FunctionalTestCase.php lines 364-380, update
createPageWithImageNodes to use $parentNode->workspaceName for both
CreateNodeAggregateWithNode and subgraph instead of the hardcoded live
workspace. In lines 399-407, default $workspace to $node->workspaceName rather
than 'live'.

255-261: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Neos.Neos.sites.* reads only the default site configuration.

Flow's ConfigurationManager does not expand * as a wildcard. It resolves the literal * key, which Neos 9 uses for default site settings. Per-site overrides under Neos.Neos.sites.example are therefore ignored, and languageUriSegment() plus getUriPathSuffix() can disagree with the site the test drives.

Add a short comment that states the literal-key intent, or resolve the concrete site node name when it is known.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Functional/FunctionalTestCase.php` around lines 255 - 261, Clarify the
literal-key behavior in siteConfigurationPath() by adding a concise comment
explaining that Neos.Neos.sites.* intentionally reads the default site settings,
or change the lookup to use the concrete site node name when the driven site is
known. Ensure languageUriSegment() and getUriPathSuffix() resolve configuration
for the same site.
Classes/Controller/SearchNodesApiController.php (1)

40-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the duplicated workspace resolution.

resolveWorkspace() and the personalWorkspaceService property are now identical in Classes/Controller/SearchNodesApiController.php, Classes/Controller/DocumentNodeListApiController.php, Classes/Controller/GetPreviewApiController.php, and Classes/Controller/NodeTreeSchemaApiController.php. Move the logic into PersonalWorkspaceService (for example resolveWorkspace(string $requestedWorkspace): string) or a shared trait. This keeps the four endpoints consistent when the fallback rule changes.

Also applies to: 141-149

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Controller/SearchNodesApiController.php` around lines 40 - 41,
Extract the duplicated workspace-resolution logic from resolveWorkspace() in
SearchNodesApiController and the corresponding methods in
DocumentNodeListApiController, GetPreviewApiController, and
NodeTreeSchemaApiController into PersonalWorkspaceService, exposing a shared
resolveWorkspace(string $requestedWorkspace): string operation. Update all four
controllers to delegate to the service and remove their duplicate resolution
code while preserving the existing fallback behavior.
Tests/Functional/Service/NodeServiceFallbackDimensionsTest.php (1)

148-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the fixture nodes exist before you read aggregateId.

getNodeByPath() can return null. Lines 153 and 182 then dereference null and the test fails with a confusing "property on null" error instead of a clear precondition failure. Add assertNotNull() after each lookup, as addressForPath() in Tests/Functional/FunctionalTestCase.php already does.

Also applies to: 177-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Functional/Service/NodeServiceFallbackDimensionsTest.php` around lines
148 - 154, Update both node lookups in the fallback-dimensions test, including
the lookup used before the second address construction, to call assertNotNull()
immediately after getNodeByPath(). Keep using each validated node’s aggregateId
so missing fixture nodes produce a clear precondition failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Classes/Service/DocumentNodeListExtractor.php`:
- Around line 86-87: Update the subgraph visibility configuration in
DocumentNodeListExtractor so the isHidden value derived from
NeosSubtreeTag::disabled() remains meaningful: use
NodeVisibility::excludeRemoved() instead of excluding disabled nodes. Preserve
the existing isHidden response mapping and continue excluding removed nodes.
- Around line 175-177: Update the path filtering around
SearchNodesApiController::searchAction() so legacy pathStartingPoint values such
as /sites/example continue matching the new /<Neos.Neos:Sites>/site/... document
paths. Normalize the requested prefix to the new absolute format before
str_starts_with(), or consistently convert the serialized root segment to /sites
in DocumentNodeListExtractor::tryRetrieveNodePath() output.
- Around line 215-220: Update the dimension-coordinate handling in the visible
extraction method to validate the point created by
DimensionSpacePoint::fromArray() against the variation graph before returning
it. Reject invalid client-supplied coordinates with an accurate error before
extract() queries content, while preserving the existing root-generalization and
createWithoutDimensions fallbacks for empty coordinates.

In `@Classes/Service/NodeService.php`:
- Around line 200-202: Qualify all three InvalidArgumentException throws in
NodeService methods find(), updatePropertiesOnNode(), and the fallback-address
write path with the global exception class, or import it explicitly at the top
of the file. Ensure unknown workspaces, missing nodes, and fallback-address
writes throw the intended global InvalidArgumentException.

In `@Classes/Service/NodeWithImageService.php`:
- Around line 99-104: Update the language filter block in the relevant
NodeWithImageService method to check that languageDimensionName is configured
before constructing ContentDimensionId or looking up the coordinate, matching
the existing guard in NodeService::nodeMatchesLanguageDimensionFilter().
Preserve the current filtering behavior when the dimension name is set.

In `@Tests/Functional/FunctionalTestCase.php`:
- Around line 267-274: Update dimensionSpacePoint() to handle a non-null
language when languageDimensionId() returns null: resolve the dimension ID
before accessing value and skip clearly instead of dereferencing null. Preserve
createWithoutDimensions() when no language is available and fromArray() when the
language dimension exists.

In `@Tests/Functional/Service/NodeServiceMultiSiteTest.php`:
- Around line 55-58: Extend the wrong-site assertions in the node result checks
around $foundNodes to cover the secondary language dimension by generating
equivalent addresses with $this->secondaryLanguage() for every excluded
/sites/example2 node, including the assertions referenced in the additional
location. Preserve the existing default-language assertions.

---

Outside diff comments:
In `@Classes/Service/DocumentNodeListExtractor.php`:
- Around line 150-163: Replace the recursive child traversal in
traverseDocuments() with a single findDescendantNodes() query using
FindDescendantNodesFilter::create(nodeTypes: $nodeTypeFilter). Remove the
per-node recursion and ensure returned documents are extracted directly,
computing depth based on document-relevant ancestry rather than all wrapping
nodes if the depth field is required.

---

Nitpick comments:
In `@Classes/Controller/SearchNodesApiController.php`:
- Around line 40-41: Extract the duplicated workspace-resolution logic from
resolveWorkspace() in SearchNodesApiController and the corresponding methods in
DocumentNodeListApiController, GetPreviewApiController, and
NodeTreeSchemaApiController into PersonalWorkspaceService, exposing a shared
resolveWorkspace(string $requestedWorkspace): string operation. Update all four
controllers to delegate to the service and remove their duplicate resolution
code while preserving the existing fallback behavior.

In `@Classes/Service/DocumentNodeListExtractor.php`:
- Line 115: Remove the unused $contentRepository parameter from
DocumentNodeListExtractor::resolveSiteNode(), and update its call site around
line 89 to stop passing that argument while preserving the remaining parameters
and behavior.

In `@Tests/Functional/FunctionalTestCase.php`:
- Around line 364-380: The fixture helpers must use the workspace associated
with their input node. In Tests/Functional/FunctionalTestCase.php lines 364-380,
update createPageWithImageNodes to use $parentNode->workspaceName for both
CreateNodeAggregateWithNode and subgraph instead of the hardcoded live
workspace. In lines 399-407, default $workspace to $node->workspaceName rather
than 'live'.
- Around line 255-261: Clarify the literal-key behavior in
siteConfigurationPath() by adding a concise comment explaining that
Neos.Neos.sites.* intentionally reads the default site settings, or change the
lookup to use the concrete site node name when the driven site is known. Ensure
languageUriSegment() and getUriPathSuffix() resolve configuration for the same
site.

In `@Tests/Functional/Service/NodeServiceFallbackDimensionsTest.php`:
- Around line 148-154: Update both node lookups in the fallback-dimensions test,
including the lookup used before the second address construction, to call
assertNotNull() immediately after getNodeByPath(). Keep using each validated
node’s aggregateId so missing fixture nodes produce a clear precondition
failure.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 090ba227-abcb-4157-9702-cbf9d2cea082

📥 Commits

Reviewing files that changed from the base of the PR and between e522e3b and 76be352.

⛔ Files ignored due to path filters (2)
  • Resources/Private/NeosUserInterface/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • Resources/Public/NeosUserInterface/Plugin.js.map is excluded by !**/*.map
📒 Files selected for processing (49)
  • .github/workflows/test.yml
  • Classes/Controller/ApplyPatchesApiController.php
  • Classes/Controller/BackendModule/AbstractFusionViewController.php
  • Classes/Controller/BackendServiceController.php
  • Classes/Controller/DocumentNodeListApiController.php
  • Classes/Controller/GetPreviewApiController.php
  • Classes/Controller/NodeTreeSchemaApiController.php
  • Classes/Controller/PreviewRenderController.php
  • Classes/Controller/SearchNodesApiController.php
  • Classes/EelHelper/NEOSidekickHelper.php
  • Classes/EelHelper/NEOSidekickInternalHelper.php
  • Classes/Factory/FindDocumentNodeDataFactory.php
  • Classes/Factory/FindImageDataFactory.php
  • Classes/Service/AbstractNodeService.php
  • Classes/Service/ContentRepositoryProvider.php
  • Classes/Service/DocumentNodeListExtractor.php
  • Classes/Service/NodeFindingService.php
  • Classes/Service/NodePatchService.php
  • Classes/Service/NodeService.php
  • Classes/Service/NodeTreeExtractor.php
  • Classes/Service/NodeTypeSchemaExtractor.php
  • Classes/Service/NodeTypeService.php
  • Classes/Service/NodeVisibility.php
  • Classes/Service/NodeWithImageService.php
  • Classes/Service/PatchValidator.php
  • Classes/Service/PersonalWorkspaceService.php
  • Classes/Service/PropertyNormalizer.php
  • Classes/Service/SearchNodesExtractor.php
  • Classes/Service/SiteService.php
  • Classes/Service/Traits/PropertyExtractionTrait.php
  • Configuration/Settings.Internal.yaml
  • Configuration/Settings.yaml
  • Configuration/Testing/NodeTypes.HomePage.yaml
  • Configuration/Testing/Settings.yaml
  • README.md
  • Resources/Private/BackendModule/BackendModule.fusion
  • Resources/Private/BackendModule/Root.fusion
  • Resources/Private/NeosUserInterface/package.json
  • Resources/Public/NeosUserInterface/Plugin.js
  • TESTING.md
  • Tests/E2E/README.md
  • Tests/Functional/FunctionalTestCase.php
  • Tests/Functional/Service/NodeServiceFallbackDimensionsTest.php
  • Tests/Functional/Service/NodeServiceImportantPagesMultiDomainTest.php
  • Tests/Functional/Service/NodeServiceMultiSiteTest.php
  • Tests/Functional/Service/NodeServiceWithImportantPagesFilterAndMultipleDimensionsAndOneSiteTest.php
  • Tests/Functional/Service/NodeServiceWithMultipleDimensionsAndOneSiteTest.php
  • Tests/Functional/Service/NodeServiceWorkspacesTest.php
  • composer.json
💤 Files with no reviewable changes (2)
  • Classes/Controller/BackendServiceController.php
  • Classes/Service/SiteService.php
🚧 Files skipped from review as they are similar to previous changes (30)
  • Configuration/Settings.yaml
  • Classes/Service/ContentRepositoryProvider.php
  • Configuration/Testing/Settings.yaml
  • Resources/Private/BackendModule/Root.fusion
  • README.md
  • composer.json
  • Configuration/Settings.Internal.yaml
  • Tests/Functional/Service/NodeServiceImportantPagesMultiDomainTest.php
  • Classes/Service/PropertyNormalizer.php
  • Classes/Factory/FindImageDataFactory.php
  • Classes/Controller/BackendModule/AbstractFusionViewController.php
  • Classes/EelHelper/NEOSidekickHelper.php
  • Tests/Functional/Service/NodeServiceWorkspacesTest.php
  • Resources/Private/BackendModule/BackendModule.fusion
  • Classes/Service/AbstractNodeService.php
  • Classes/Service/NodeTypeSchemaExtractor.php
  • Tests/Functional/Service/NodeServiceWithMultipleDimensionsAndOneSiteTest.php
  • Classes/EelHelper/NEOSidekickInternalHelper.php
  • Classes/Factory/FindDocumentNodeDataFactory.php
  • Tests/Functional/Service/NodeServiceWithImportantPagesFilterAndMultipleDimensionsAndOneSiteTest.php
  • Classes/Service/PatchValidator.php
  • Classes/Service/NodeTypeService.php
  • Classes/Service/SearchNodesExtractor.php
  • Classes/Service/NodeTreeExtractor.php
  • Classes/Controller/PreviewRenderController.php
  • Classes/Service/Traits/PropertyExtractionTrait.php
  • Classes/Service/NodePatchService.php
  • Classes/Service/NodeVisibility.php
  • TESTING.md
  • Configuration/Testing/NodeTypes.HomePage.yaml

Comment on lines +86 to +87
$subgraph = $contentRepository->getContentGraph($workspaceObject->workspaceName)
->getSubgraph($dimensionSpacePoint, NodeVisibility::excludeDisabledAndRemoved());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

isHidden in the response is now always false.

The subgraph uses NodeVisibility::excludeDisabledAndRemoved(), so disabled nodes are never returned. Line 183 still derives isHidden from NeosSubtreeTag::disabled(). That field can therefore never be true, and API consumers lose the previous signal.

Pick one behavior:

  • Keep frontend-like reading and drop isHidden, or document it as always false.
  • Or read with NodeVisibility::excludeRemoved() so disabled nodes stay visible and isHidden stays meaningful.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/DocumentNodeListExtractor.php` around lines 86 - 87, Update
the subgraph visibility configuration in DocumentNodeListExtractor so the
isHidden value derived from NeosSubtreeTag::disabled() remains meaningful: use
NodeVisibility::excludeRemoved() instead of excluding disabled nodes. Preserve
the existing isHidden response mapping and continue excluding removed nodes.

Comment on lines +175 to +177
// NOTE (Neos 9 migration decision): node paths now use the absolute path format
// "/<Neos.Neos:Sites>/site/..." instead of the legacy "/sites/site/..." format.
'path' => $this->tryRetrieveNodePath($subgraph, $node) ?? '',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The new path format silently breaks pathStartingPoint filtering.

SearchNodesApiController::searchAction() filters this result with str_starts_with($document['path'], $pathStartingPoint). The path now starts with /<Neos.Neos:Sites>/. A caller that passes a legacy prefix such as /sites/example receives an empty document list and no error.

Normalize the prefix in the filter, or map the serialized root segment to /sites before returning path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/DocumentNodeListExtractor.php` around lines 175 - 177, Update
the path filtering around SearchNodesApiController::searchAction() so legacy
pathStartingPoint values such as /sites/example continue matching the new
/<Neos.Neos:Sites>/site/... document paths. Normalize the requested prefix to
the new absolute format before str_starts_with(), or consistently convert the
serialized root segment to /sites in
DocumentNodeListExtractor::tryRetrieveNodePath() output.

Comment on lines +215 to +220
if ($coordinates !== []) {
return DimensionSpacePoint::fromArray($coordinates);
}
$rootGeneralizations = $contentRepository->getVariationGraph()->getRootGeneralizations();
return $rootGeneralizations !== [] ? reset($rootGeneralizations) : DimensionSpacePoint::createWithoutDimensions();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the variation graph API and DimensionSpacePoint factory in the installed CR.
fd -t f 'InterDimensionalVariationGraph.php' | xargs -r -I{} rg -n -A12 'function getRootGeneralizations' {}
fd -t f 'DimensionSpacePoint.php' -p 'ContentRepository' | xargs -r -I{} rg -n -A15 'public static function fromArray' {}

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository top-level files/dirs:\n'
git ls-files | sed 's#/.*##' | sort -u | head -100

printf '\nFind target file:\n'
fd -t f 'DocumentNodeListExtractor.php' . || true

printf '\nFind Neos variation graph / dimension point candidates in tracked files:\n'
git ls-files | rg -i 'InterDimensionalVariationGraph|DimensionSpacePoint|ContentRepositoryVariant|DocumentNodeListExtractor' || true

printf '\nInspect target file around resolveDimensionSpacePoint:\n'
file="$(fd -t f 'DocumentNodeListExtractor.php' . | head -n 1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '1,260p' "$file" | cat -n
fi

Repository: NEOSidekick/NEOSidekick.AiAssistant

Length of output: 11516


🌐 Web query:

Neos 9 ContentRepository getVariationGraph getRootGeneralizations DimensionSpacePoint signature

💡 Result:

In Neos 9.0, the InterDimensionalVariationGraph provides methods to interact with the dimension space. The getRootGeneralizations method is used to retrieve the root dimension space points within the variation graph [1]. The signature for getRootGeneralizations in the Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph class is: public function getRootGeneralizations: array This method returns an array of DimensionSpacePoint objects representing the root generalizations, indexed by their hash [1]. It is guaranteed to return at least one element, representing the empty dimension space point in a zero-dimensional content repository [1][2]. To access the variation graph from the ContentRepository instance, you use the getVariationGraph() method [3][4]. The workflow typically looks like this: $contentRepository = $contentRepositoryRegistry->get($contentRepositoryId); $variationGraph = $contentRepository->getVariationGraph; $rootGeneralizations = $variationGraph->getRootGeneralizations; The DimensionSpacePoint is a core concept in Neos 9, representing a specific point in the dimension space defined by coordinates (DimensionName => DimensionValue) [5]. It is frequently used in conjunction with the variation graph to determine generalizations, specializations, or to navigate content subgraphs [1][6].

Citations:


🌐 Web query:

Neos 9 DimensionSpacePoint fromArray source code

💡 Result:

In Neos 9, the DimensionSpacePoint class is defined within the Neos.ContentRepository.Core package [1]. The static method fromArray(array $data) is used to create a DimensionSpacePoint instance from an array of coordinate data [1]. While the full implementation detail is managed through the inheritance chain in AbstractDimensionSpacePoint.php [2], the static factory method is provided to ensure proper instantiation of dimension space points based on the provided coordinate mapping [1]. You can view the specific API documentation for this method at the official Neos 9.0 documentation portal [1]. Additionally, usage of this method can be seen in components such as DimensionSpacePointSet, which utilizes it when iterating over point candidates [3], or in frontend routing resolution logic [4].

Citations:


Validate the client-supplied dimension coordinates before querying the subgraph.

getRootGeneralizations() returns an array of DimensionSpacePoint objects, so the fallback return type is correct. If any dimension array value is not a valid dimension value in the variation graph, DimensionSpacePoint::fromArray() returns an empty point and the request fails with No site found; validate the built point against the variation graph or throw a more accurate error before extract() queries content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/DocumentNodeListExtractor.php` around lines 215 - 220, Update
the dimension-coordinate handling in the visible extraction method to validate
the point created by DimensionSpacePoint::fromArray() against the variation
graph before returning it. Reject invalid client-supplied coordinates with an
accurate error before extract() queries content, while preserving the existing
root-generalization and createWithoutDimensions fallbacks for empty coordinates.

Comment on lines 200 to 202
if (!$workspace) {
throw new InvalidArgumentException('The given workspace does not exist in the database. Please reload the page.', 1713440899886);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

InvalidArgumentException is unqualified and unimported — every throw fatals.

This file declares namespace NEOSidekick\AiAssistant\Service and imports no InvalidArgumentException (Lines 7-31). The bare name therefore resolves to NEOSidekick\AiAssistant\Service\InvalidArgumentException, which does not exist. All three throw sites fail with a class-not-found fatal error instead of the intended exception:

  • Line 201: an unknown workspace in find().
  • Line 273: a missing node in updatePropertiesOnNode().
  • Line 282: a write to a fallback address.

Tests/Functional/Service/NodeServiceFallbackDimensionsTest.php imports the global InvalidArgumentException on Line 5 and asserts it on Line 159, so updateRejectsWritesToFallbackAddresses cannot pass with the current code.

🐛 Proposed fix
 use Neos\ContentRepository\Core\Dimension\ContentDimensionId;
+use InvalidArgumentException;

Also applies to: 272-288

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodeService.php` around lines 200 - 202, Qualify all three
InvalidArgumentException throws in NodeService methods find(),
updatePropertiesOnNode(), and the fallback-address write path with the global
exception class, or import it explicitly at the top of the file. Ensure unknown
workspaces, missing nodes, and fallback-address writes throw the intended global
InvalidArgumentException.

Comment on lines +99 to +104
if (!empty($languageDimensionFilter)) {
$languageCoordinate = $documentNodeAddress->dimensionSpacePoint->getCoordinate(new ContentDimensionId($this->languageDimensionName));
if ($languageCoordinate !== null && !in_array($languageCoordinate, $languageDimensionFilter, true)) {
continue;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the language dimension lookup when languageDimensionName is not configured.

$this->languageDimensionName comes from @Flow\InjectConfiguration(path="languageDimensionName"). If the setting is absent, the property stays null and new ContentDimensionId(null) on Line 100 raises a TypeError. NodeService::nodeMatchesLanguageDimensionFilter() guards this case with isset($this->languageDimensionName). Apply the same guard here so both services behave the same for distributions without a language dimension.

🛡️ Proposed guard
-            if (!empty($languageDimensionFilter)) {
+            if (!empty($languageDimensionFilter) && !empty($this->languageDimensionName)) {
                 $languageCoordinate = $documentNodeAddress->dimensionSpacePoint->getCoordinate(new ContentDimensionId($this->languageDimensionName));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!empty($languageDimensionFilter)) {
$languageCoordinate = $documentNodeAddress->dimensionSpacePoint->getCoordinate(new ContentDimensionId($this->languageDimensionName));
if ($languageCoordinate !== null && !in_array($languageCoordinate, $languageDimensionFilter, true)) {
continue;
}
}
if (!empty($languageDimensionFilter) && !empty($this->languageDimensionName)) {
$languageCoordinate = $documentNodeAddress->dimensionSpacePoint->getCoordinate(new ContentDimensionId($this->languageDimensionName));
if ($languageCoordinate !== null && !in_array($languageCoordinate, $languageDimensionFilter, true)) {
continue;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Classes/Service/NodeWithImageService.php` around lines 99 - 104, Update the
language filter block in the relevant NodeWithImageService method to check that
languageDimensionName is configured before constructing ContentDimensionId or
looking up the coordinate, matching the existing guard in
NodeService::nodeMatchesLanguageDimensionFilter(). Preserve the current
filtering behavior when the dimension name is set.

Comment on lines +267 to 274
protected function dimensionSpacePoint(?string $language = null): DimensionSpacePoint
{
$nodeTemplate = new NodeTemplate();
$nodeTemplate->setNodeType($this->nodeTypeManager->getNodeType('NEOSidekick.AiAssistant.Testing:Image'));
$nodeTemplate->setProperty('image', $this->importImage($imageFixtureFilename));
return $nodeTemplate;
$language = $language ?? $this->primaryLanguage();

return $language === null
? DimensionSpacePoint::createWithoutDimensions()
: DimensionSpacePoint::fromArray([$this->languageDimensionId()->value => $language]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Null dereference when a language is passed and no language dimension exists.

languageDimensionId() returns null on distributions without the configured language dimension. Line 273 then calls ->value on null and the test fails with a fatal error instead of a clear skip. The check on line 271 only covers $language === null.

🐛 Proposed fix
     protected function dimensionSpacePoint(?string $language = null): DimensionSpacePoint
     {
         $language = $language ?? $this->primaryLanguage();
+        $dimensionId = $this->languageDimensionId();
 
-        return $language === null
+        return ($language === null || $dimensionId === null)
             ? DimensionSpacePoint::createWithoutDimensions()
-            : DimensionSpacePoint::fromArray([$this->languageDimensionId()->value => $language]);
+            : DimensionSpacePoint::fromArray([$dimensionId->value => $language]);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected function dimensionSpacePoint(?string $language = null): DimensionSpacePoint
{
$nodeTemplate = new NodeTemplate();
$nodeTemplate->setNodeType($this->nodeTypeManager->getNodeType('NEOSidekick.AiAssistant.Testing:Image'));
$nodeTemplate->setProperty('image', $this->importImage($imageFixtureFilename));
return $nodeTemplate;
$language = $language ?? $this->primaryLanguage();
return $language === null
? DimensionSpacePoint::createWithoutDimensions()
: DimensionSpacePoint::fromArray([$this->languageDimensionId()->value => $language]);
}
protected function dimensionSpacePoint(?string $language = null): DimensionSpacePoint
{
$language = $language ?? $this->primaryLanguage();
$dimensionId = $this->languageDimensionId();
return ($language === null || $dimensionId === null)
? DimensionSpacePoint::createWithoutDimensions()
: DimensionSpacePoint::fromArray([$dimensionId->value => $language]);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Functional/FunctionalTestCase.php` around lines 267 - 274, Update
dimensionSpacePoint() to handle a non-null language when languageDimensionId()
returns null: resolve the dimension ID before accessing value and skip clearly
instead of dereferencing null. Preserve createWithoutDimensions() when no
language is available and fromArray() when the language dimension exists.

Comment on lines +55 to +58
$this->assertArrayNotHasKey($this->addressForPath('/sites/example2', $this->currentUserWorkspace), $foundNodes);
$this->assertArrayNotHasKey($this->addressForPath('/sites/example2/node-two-wan-kenodi', $this->currentUserWorkspace), $foundNodes);
$this->assertArrayNotHasKey($this->addressForPath('/sites/example2/node-two-wan-kenodi/lady-eleonode-rootford-2', $this->currentUserWorkspace), $foundNodes);
$this->assertArrayNotHasKey($this->addressForPath('/sites/example2/node-two-mc-nodeface', $this->currentUserWorkspace), $foundNodes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check wrong-site nodes in every language dimension.

The setup creates secondary-language variants for both sites. These assertions only reject addresses from the default dimension.

A wrong-site secondary-language node can remain in $foundNodes without failing the tests. Add equivalent assertions with $this->secondaryLanguage(), or verify the site of every returned address.

Also applies to: 78-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Functional/Service/NodeServiceMultiSiteTest.php` around lines 55 - 58,
Extend the wrong-site assertions in the node result checks around $foundNodes to
cover the secondary language dimension by generating equivalent addresses with
$this->secondaryLanguage() for every excluded /sites/example2 node, including
the assertions referenced in the additional location. Preserve the existing
default-language assertions.

main is renamed to 4.x (Neos 9 line); 3.x carries the Neos 8.3/8.4 line.
The temporary feature-branch trigger is no longer needed.
@gradinarufelix
gradinarufelix merged commit ed1c3ec into main Aug 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant