Skip to content

feat(extract): fix cross-module connection detection regression (#269) - #270

Merged
NTCoding merged 3 commits into
mainfrom
issue-269-riviere-extract-must-resolve-a
Feb 22, 2026
Merged

NTCoding merged 3 commits into
mainfrom
issue-269-riviere-extract-must-resolve-a

Conversation

@NTCoding

@NTCoding NTCoding commented Feb 22, 2026 •

Copy link
Copy Markdown
Owner

Closes #269

Problem

riviere extract fails to detect 15 cross-module event subscription connections. Regression from PR #267 (per-module ts-morph Projects).

Verified by npm run verify:connections in ecommerce-demo-app:

Missing connections (15 false negatives):
  - [async] shipping:event:ShipmentDispatched -> orders:eventHandler:handle
  - [async] shipping:event:ShipmentDelivered -> orders:eventHandler:handle
  - [async] payment:event:PaymentFailed -> orders:eventHandler:handle
  - [async] payment:event:PaymentCompleted -> orders:eventHandler:handle
  - [async] inventory:event:InventoryReserved -> orders:eventHandler:handle
  - [async] orders:event:OrderConfirmed -> shipping:eventHandler:OrderConfirmedHandler
  - [async] shipping:event:ShipmentCreated -> inventory:eventHandler:ShipmentCreatedHandler
  - [async] orders:event:OrderPlaced -> inventory:eventHandler:OrderPlacedHandler
  - [async] orders:event:OrderCancelled -> inventory:eventHandler:OrderCancelledHandler
  - [async] orders:event:OrderPlaced -> payment:eventHandler:OrderPlacedHandler
  - [async] orders:event:OrderCancelled -> payment:eventHandler:OrderCancelledHandler
  - [async] shipping:event:ShipmentDispatched -> notifications:eventHandler:ShipmentDispatchedHandler
  - [async] shipping:event:ShipmentDelivered -> notifications:eventHandler:ShipmentDeliveredHandler
  - [async] payment:event:PaymentCompleted -> notifications:eventHandler:PaymentCompletedHandler
  - [async] orders:event:OrderPlaced -> notifications:eventHandler:OrderPlacedHandler

Root Cause

File: packages/riviere-cli/src/features/extract/domain/detect-connections-per-module.ts

// Line 25: filters to module-only components
const moduleComponents = enrichedComponents.filter((c) => c.domain === ctx.module.name)
// Line 30: passes only module components to detectConnections
const result = detectConnections(ctx.project, moduleComponents, ...)

detectConnections (packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/detect-connections.ts) bundles 4 strategies. Two do cross-module metadata matching and break when given only one module's components:

Strategy How it works Needs Project Needs all components Works per-module
buildCallGraph (line 77) AST traversal of source files YES NO ✅ YES
detectConfigurableConnections (line 96) AST pattern matching against user-defined patterns YES NO ✅ YES
detectSubscribeConnections (line 89) Metadata join: handler.metadata['subscribedEvents'] ↔ event.metadata['eventName'] NO YES ❌ NO
detectPublishConnections (line 85) Metadata join: publisher.metadata['publishedEventType'] ↔ event.metadata['eventName'] NO YES ❌ NO

Subscribe: when processing the orders module, events like ShipmentDispatched (shipping module) aren't in scope → _unresolved.

Publish: same cross-module matching problem. Currently hidden in ecommerce-demo-app because publishers and events are co-located within modules, but any codebase with cross-module publish→event relationships will break.

Implementation

Step 1: Create detectPerModuleConnections in riviere-extract-ts

File: packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/detect-connections.ts

New function that runs only source-scanning strategies (need a Project, work per-module):

  • buildCallGraph
  • detectConfigurableConnections
export function detectPerModuleConnections(
  project: Project,
  components: readonly EnrichedComponent[],
  options: ConnectionDetectionOptions,
  globMatcher: GlobMatcher,
): { links: ExtractedLink[], timings: Partial<ConnectionTimings> }

Step 2: Create detectCrossModuleConnections in riviere-extract-ts

File: same as Step 1

New function that runs metadata-matching strategies (need all components, run once after per-module loop):

  • detectSubscribeConnections
  • detectPublishConnections

Both are pure metadata matching — no AST/Project needed. The previous AST fallback in detectPublishConnections was removed (SD-001 violation: fail-fast over silent fallbacks). When publishedEventType metadata is missing, strict mode throws ConnectionDetectionError and lenient mode returns an _uncertain link.

export function detectCrossModuleConnections(
  allComponents: readonly EnrichedComponent[],
  options: Pick<ConnectionDetectionOptions, 'allowIncomplete' | 'repository'>,
): { links: ExtractedLink[], timings: Partial<ConnectionTimings> }

Inside this function:

  • detectPublishConnections is called once with all components
  • detectSubscribeConnections is called once with all components

Step 3: Update detectConnectionsPerModule in riviere-cli

File: packages/riviere-cli/src/features/extract/domain/detect-connections-per-module.ts

Two-phase orchestration:

export function detectConnectionsPerModule(
  moduleContexts: ModuleContext[],
  enrichedComponents: EnrichedComponent[],
  repositoryName: string,
  allowIncomplete: boolean,
): PerModuleConnectionResult {
  const links: ExtractedLink[] = []
  const timings: ConnectionTimings[] = []

  // Phase 1: Per-module — source-scanning strategies (need Project + module source files)
  for (const ctx of moduleContexts) {
    const moduleComponents = enrichedComponents.filter((c) => c.domain === ctx.module.name)
    if (moduleComponents.length === 0) continue
    const result = detectPerModuleConnections(ctx.project, moduleComponents, {
      allowIncomplete,
      moduleGlobs: [posix.join(ctx.module.path, ctx.module.glob)],
      repository: repositoryName,
    }, matchesGlob)
    links.push(...result.links)
    timings.push(result.timings)
  }

  // Phase 2: Cross-module — metadata-matching strategies (need ALL components)
  const crossModuleResult = detectCrossModuleConnections(
    enrichedComponents,
    { allowIncomplete, repository: repositoryName },
  )
  links.push(...crossModuleResult.links)

  return { links: deduplicateCrossStrategy(links), timings }
}

Step 4: Export new functions from riviere-extract-ts

File: packages/riviere-extract-ts/src/index.ts

Export detectPerModuleConnections and detectCrossModuleConnections. Keep existing detectConnections export for backward compatibility.

Step 5: Tests

Unit tests in riviere-extract-ts (new spec files or extend detect-connections.spec.ts):

  • detectPerModuleConnections — produces call graph and configurable links only, no async links
  • detectCrossModuleConnections — produces subscribe and publish links, resolves cross-module matches where handler is in module A and event is in module B
  • Edge cases: empty components, single module, all components in one module, publisher without publishedEventType metadata (strict throws, lenient returns _uncertain)

Integration test in riviere-cli (detect-connections-per-module.spec.ts — new):

  • Two modules: events in module A, handlers in module B → subscribe links resolve
  • Publisher in module A, event in module B → publish links resolve
  • Deduplication works across both phases

Files to Modify

File Change
packages/riviere-extract-ts/.../detect-connections.ts Add detectPerModuleConnections and detectCrossModuleConnections
packages/riviere-extract-ts/src/index.ts Export new functions
packages/riviere-cli/.../detect-connections-per-module.ts Two-phase orchestration
packages/riviere-extract-ts/.../detect-connections.spec.ts Tests for new functions
packages/riviere-cli/.../detect-connections-per-module.spec.ts (new) Integration tests

Acceptance Criteria

  • All 15 cross-module subscribe links resolve correctly (no _unresolved or _uncertain)
  • Publish connections still work (no regression — all publish links in ecommerce-demo-app continue to pass)
  • --allow-incomplete flag still works
  • 100% test coverage maintained in riviere-extract-ts and riviere-cli
  • npm run verify:connections in ecommerce-demo-app shows exactly 4 remaining failures: 2 missing HTTP links + 2 missing external links (these are out of scope for this issue)

Verification

# In ecommerce-demo-app after implementing this issue:
npm run verify:connections

# Expected output:
# Extracted N total links (N certain, 0 uncertain)
# Extracted 0 external links
# Connection mismatch!
#
# Missing connections (2 false negatives):
#   - [sync] bff:useCase:PlaceOrderBFFUseCase -> orders:api:handle
#   - [sync] bff:useCase:PlaceOrderBFFUseCase -> inventory:api:CheckStockEndpoint
# Missing external links (2 false negatives):
#   - [sync] bff:useCase:PlaceOrderBFFUseCase -> Fraud Detection Service (external)
#   - [sync] bff:useCase:PlaceOrderBFFUseCase -> Customer Service (external)
#
# The 15 subscribe connections that were previously missing should all pass.
# The 4 remaining failures (2 HTTP + 2 external) are tracked in a separate issue.

Amendment

AST fallback in detectPublishConnections was intentionally removed (SD-001 violation - fail-fast over silent fallbacks). When publishedEventType metadata is missing, strict mode throws ConnectionDetectionError and lenient mode returns _uncertain link. This eliminates the need for Map<string, Project> in detectCrossModuleConnections - it now takes only allComponents. The ecommerce-demo-app verify:connections check requires manual post-merge verification.

Summary by CodeRabbit

  • New Features

    • Per-module and cross-module connection detection with aggregated timing and deduplicated links.
    • Publish–subscribe detection now resolves via component metadata for more reliable event matching.
  • Tests

    • Added comprehensive unit tests covering per-module, cross-module, async publish detection, timing aggregation, and metadata strict/lenient behaviors.
  • Chores

    • Minor script path calculation tweak for worktree creation.

= and others added 2 commits February 22, 2026 16:54
Split connection detection into two phases: per-module source-scanning
(AST strategies needing a Project) and cross-module metadata-matching
(subscribe/publish strategies needing all components). Remove dangerous
AST fallback in detectPublishConnections (SD-001 violation) — missing
publishedEventType metadata now fails fast (strict) or returns
_uncertain (lenient).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Feb 22, 2026 •

Copy link
Copy Markdown

Deploying living-architecture with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9826579
Status: ✅  Deploy successful!
Preview URL: https://ddb91cd2.living-architecture.pages.dev
Branch Preview URL: https://issue-269-riviere-extract-mu.living-architecture.pages.dev

View logs

@coderabbitai

coderabbitai Bot commented Feb 22, 2026 •

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Splits detection into per-module (source-scanning) and cross-module (metadata-matching) phases, adds detectPerModuleConnections and detectCrossModuleConnections, aggregates and deduplicates links, and changes detectPublishConnections to metadata-driven resolution without a Project parameter. Timings are restructured per-phase.

Changes

Cohort / File(s) Summary
Core detection architecture
packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/detect-connections.ts
Adds per-module and cross-module types and entry points (detectPerModuleConnections, detectCrossModuleConnections), introduces runConfigurableDetection, instruments per-phase timings, and changes detection orchestration to separate source-scanning and metadata-matching.
Publish connection detection
packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/async-detection/detect-publish-connections.ts, .../detect-publish-connections.spec.ts
Removes Project parameter, resolves publishers via publishedEventType metadata, adds handleMissingMetadata behavior (strict errors vs lenient uncertain links), and updates tests to metadata-driven scenarios.
CLI orchestration & tests
packages/riviere-cli/src/features/extract/domain/detect-connections-per-module.ts, .../detect-connections-per-module.spec.ts
Replaces single detectConnections call with two-phase flow: per-module loop calling detectPerModuleConnections, then a single detectCrossModuleConnections; aggregates timings and deduplicates links; adds integration tests validating invocation, options propagation, and timing aggregation.
Per-/Cross-module unit tests
packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/detect-per-module-connections.spec.ts, .../detect-cross-module-connections.spec.ts
Adds tests for per-module call-graph/configurable detection, moduleGlobs filtering, and cross-module publish/subscribe resolution, strict/lenient behaviors, empty input, and timing presence.
Test fixture update
packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/detect-connections.spec.ts
Updates OrderPublisher fixture to include publishedEventType: 'OrderPlacedEvent' metadata to align with metadata-driven publish resolution.
Public exports
packages/riviere-extract-ts/src/index.ts
Re-exports new functions/types: detectPerModuleConnections, detectCrossModuleConnections, deduplicateCrossStrategy, and new PerModule/CrossModule types.
Utility script
scripts/start-task.sh
Minor change to worktree path calculation (derive parent directory dynamically).

Sequence Diagram

sequenceDiagram
    participant CLI as CLI: detect-connections-per-module
    participant PM as Per-Module Detection
    participant CM as Cross-Module Detection
    participant DD as Deduplication

    CLI->>CLI: iterate ModuleContexts
    loop per-module
        CLI->>PM: detectPerModuleConnections(project, moduleComponents, options, matchesGlob)
        PM->>PM: buildCallGraph + detectConfigurableConnections
        PM-->>CLI: {links: [sync/configurable], timings: {callGraphMs, configurableMs, setupMs}}
        CLI->>CLI: accumulate links & timings
    end

    CLI->>CM: detectCrossModuleConnections(allComponents, {allowIncomplete, repository})
    CM->>CM: detectPublishConnections(allComponents)
    CM->>CM: detectSubscribeConnections(allComponents)
    CM-->>CLI: {links: [publish, subscribe], timings: {asyncDetectionMs}}
    
    CLI->>DD: deduplicateCrossStrategy(accumulatedLinks + crossLinks)
    DD-->>CLI: {links: deduplicated}
    CLI-->>CLI: return PerModuleConnectionResult (links, timings[])
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • NTCoding/living-architecture#256: Modifies detect-connections and timing/phase split; strong overlap with per-module vs cross-module detection and deduplication changes.
  • NTCoding/living-architecture#252: Alters connection-detection pipeline and typings; likely touches same functions/types exported from detect-connections.
  • NTCoding/living-architecture#251: Related changes to async publish/subscribe detection and event/publisher resolution logic.

Suggested labels

Review effort 4/5

Poem

🐇 I hopped through modules, sniffed metadata bright,
Per-module first, then cross-module light,
Events found their handlers, publishers too,
Timings in pockets, duplicates few,
A rabbit cheers code that now links true!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR contains one minor out-of-scope change: scripts/start-task.sh worktree path calculation alteration is unrelated to connection detection objectives. Consider removing the scripts/start-task.sh change or clarifying its relationship to the regression fix in the PR description.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: fixing a cross-module connection detection regression in the extract feature.
Linked Issues check ✅ Passed All major implementation objectives from issue #269 are addressed: detectPerModuleConnections and detectCrossModuleConnections functions added, two-phase orchestration implemented in CLI, AST fallback removed from publish detection, new exports added, comprehensive tests added for per/cross-module detection and integration testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-269-riviere-extract-must-resolve-a

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.16.0)
packages/riviere-cli/src/features/extract/domain/detect-connections-per-module.spec.ts

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m

packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/async-detection/detect-publish-connections.spec.ts

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m


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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/async-detection/detect-publish-connections.spec.ts (1)

200-218: ⚠️ Potential issue | 🟡 Minor

Case-sensitivity test assertion is weaker than the test title claims.

The title states "uses exact case-sensitive matching for publishedEventType to eventName", but the assertion only checks that _uncertain contains 'orderplaced'. Contrast with the analogous lenient/no-match test (lines 80–87) that also asserts source, target: '_unresolved', and type: 'async'. An implementation that returned a resolved link with _uncertain set would still pass this test, which contradicts the claim in the title.

✅ Proposed fix — strengthen the assertion to match the claim
     expect(result).toStrictEqual([
-      expect.objectContaining({ _uncertain: expect.stringContaining('orderplaced') }),
+      expect.objectContaining({
+        source: 'orders:eventPublisher:CasePublisher',
+        target: '_unresolved',
+        type: 'async',
+        _uncertain: expect.stringContaining('orderplaced'),
+      }),
     ])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/async-detection/detect-publish-connections.spec.ts`
around lines 200 - 218, The test title claims case-sensitive matching but the
assertion only checks _uncertain contains 'orderplaced', so strengthen the
assertion to mirror the analogous lenient/no-match test: assert that
detectPublishConnections returns a single unresolved async link with source
equal to the event fixture (the variable event), target equal to '_unresolved',
type equal to 'async', and still assert _uncertain contains 'orderplaced';
update the expectation to use expect.objectContaining with those fields against
the result array returned by detectPublishConnections.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/riviere-cli/src/features/extract/domain/detect-connections-per-module.spec.ts`:
- Around line 34-47: The createModule helper currently returns a full Module
with many `{ notUsed: true }` placeholders which is fragile if the Module type
gains required fields; update createModule to return a Partial<Module> or
explicitly cast the result (e.g. `return { name, path, glob, api: {}, ... } as
Module` or change the function signature to `createModule(name: string):
Partial<Module>`), or extract a shared test-fixture builder that constructs only
the fields tests rely on (refer to the createModule function and the Module
type) so type changes will surface in tests rather than silently diverge.
- Around line 88-117: The test for detectConnectionsPerModule currently only
asserts mockDetectPerModule call count; update it to also assert the call
arguments to ensure per-module filtering: after invoking
detectConnectionsPerModule, add expectations that mockDetectPerModule was called
with ordersCtx.project and [orderComp] for the first call and with
shippingCtx.project and [shippingComp] for the second call (use
expect.any(Object) for the options param and the existing mockMatchesGlob for
the glob matcher) so createModuleContext/createComponent pairing is validated.

In
`@packages/riviere-cli/src/features/extract/domain/detect-connections-per-module.ts`:
- Around line 44-50: The ConnectionTimings shape is being used for two different
semantics (per-module vs cross-module) which causes unused/zeroed fields;
introduce a discriminated union (e.g., type ConnectionTiming = PerModuleTiming |
CrossModuleTiming) or two separate types and add a discriminator field like
kind: 'perModule'|'crossModule', then update the creation sites (the
timings.push call that currently sets asyncDetectionMs=0 and the cross-module
push sites that zero callGraphMs/configurableMs/setupMs) to construct the
appropriate variant and update any consumers to narrow on the discriminator
before reading fields (search for usages of ConnectionTimings, timings.push, and
any code reading asyncDetectionMs/callGraphMs/configurableMs/setupMs).

In
`@packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/async-detection/detect-publish-connections.spec.ts`:
- Around line 55-67: Update the strict-mode tests that currently only assert the
error type to also assert an identifying message substring: for the test titled
"throws ConnectionDetectionError in strict mode when publishedEventType matches
no event" (and the similar ambiguous-match and missing-metadata tests), change
the expectation from just .toThrow(ConnectionDetectionError) to assert both the
type and message (e.g., using expect(() =>
detectPublishConnections(...)).toThrowError(new ConnectionDetectionError(...))
or expect(() => ...).toThrowError(/substring-of-expected-message/) so the test
verifies the ConnectionDetectionError contains a specific identifying message;
reference detectPublishConnections and ConnectionDetectionError when making the
change.

---

Outside diff comments:
In
`@packages/riviere-extract-ts/src/features/extraction/domain/connection-detection/async-detection/detect-publish-connections.spec.ts`:
- Around line 200-218: The test title claims case-sensitive matching but the
assertion only checks _uncertain contains 'orderplaced', so strengthen the
assertion to mirror the analogous lenient/no-match test: assert that
detectPublishConnections returns a single unresolved async link with source
equal to the event fixture (the variable event), target equal to '_unresolved',
type equal to 'async', and still assert _uncertain contains 'orderplaced';
update the expectation to use expect.objectContaining with those fields against
the result array returned by detectPublishConnections.

- Add per-module component filtering assertions (nthCalledWith)
- Verify error messages in strict-mode throw assertions
- Remove unused ConnectionDetectionError import

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@sonarqubecloud

Copy link
Copy Markdown

@NTCoding
NTCoding merged commit 906e2fa into main Feb 22, 2026
17 checks 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.

riviere extract must resolve all connections within extraction scope

1 participant