Skip to content

Conversation

@issuetopr-dev
Copy link

@issuetopr-dev issuetopr-dev bot commented Jan 2, 2026

Context
This PR is the second step after introducing the StorageAdapter infra. It updates workflow creation paths to actually use the StorageAdapter and persist Repository, User/GithubUser, and attribution data for new workflow runs. It also enhances the GitHub webhook types to carry full repository metadata.

Summary of changes

  • Webhook payloads

    • lib/webhook/github/types.ts: Expand IssuesPayloadSchema to include repository.id, node_id, full_name, name, owner.login, default_branch, visibility, has_issues; sender.id and sender.login.
  • resolveIssue workflow

    • lib/workflows/resolveIssue.ts: Replace initializeWorkflowRun with StorageAdapter.workflow.run.create and pass complete repository metadata and actor attribution.
    • Actor handling:
      • Webhook-triggered: type "webhook", source "github", with sender and installationId from webhook
      • User/system-triggered: type "user" (system placeholder for now)
    • Visibility normalized to PUBLIC | PRIVATE | INTERNAL.
  • Webhook handlers

    • lib/webhook/github/handlers/issue/label.resolve.handler.ts: Pass webhookContext (event/action/sender/installationId) to resolveIssue. Also use neo4jDs.getSession('READ') for settings.
  • Workers orchestrator

    • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts: Use singleton neo4jDs from ../neo4j and use getSession('READ') for settings. Removed local data source creation.
  • Other action files

    • lib/actions/resolveIssue.ts, lib/actions/workflows/autoResolveIssue.ts, lib/actions/workflows/createDependentPR.ts: Use neo4jDs.getSession('READ') explicitly for settings reads.
  • Cleanup deprecated functions

    • lib/neo4j/repositories/workflowRun.ts and shared/src/lib/neo4j/repositories/workflowRun.ts: Remove deprecated linkToIssue.
    • lib/neo4j/services/workflow.ts: Keep listWorkflowRuns but mark with @deprecated in favor of StorageAdapter.workflow.run.list.

Notes and considerations

  • Repository owner.login is normalized from the repository object; falls back to the repoFullName owner if missing.
  • defaultBranch may be undefined; we pass undefined intentionally when missing.
  • This PR lays groundwork for commit attribution; StorageAdapter.create accepts optional commit, ready for future PRs.

Why this approach

  • Aligns write-path with the new StorageAdapter so new runs create Repository/GithubUser nodes and relationships in a single, transactionally safe place.
  • Centralizes Neo4j session management via neo4jDs and read/write semantics inside the StorageAdapter.

Testing

  • Lint: pnpm run lint (passes, zero warnings)
  • Manual: Label an issue via webhook. Verify in Neo4j:
    • Repository node created/updated with full metadata
    • GithubUser node created/linked via INITIATED_BY
    • WorkflowRun -> Repository (BASED_ON_REPOSITORY), WorkflowRun -> Issue (BASED_ON_ISSUE), WorkflowRun -> GithubUser (INITIATED_BY)

Follow-ups

  • PR Fix issue 1 #3 will provide a migration to backfill Repository nodes for old runs.
  • UI will be updated to leverage the new model in a subsequent PR.

Closes: Part of the epic to migrate WorkflowRun data model; depends on StorageAdapter infra from previous PR.

Closes #1446


Update (follow-up after review)

Addressed feedback from @youngchingjui:

  1. Test coverage for schema changes
  • Updated webhook route tests to reflect expanded IssuesPayloadSchema requirements:
    • tests/api/webhook/github.route.test.ts
    • tests/api/webhook/github/route.test.ts
    • Tests now include repository.id, repository.node_id, repository.name, repository.owner.login, and sender.id.
  • Ran tests with coverage to verify: pnpm run test:coverage
    • All suites passing (15/15). Coverage report generated locally.
  1. Deprecation annotations for legacy workflow creation
  • Added JSDoc @deprecated to initializeWorkflowRun with guidance to use StorageAdapter.workflow.run.create:
    • lib/neo4j/services/workflow.ts
    • shared/src/lib/neo4j/services/workflow.ts
  • Also marked listWorkflowRuns as @deprecated in the shared service for consistency (aligned with previous note in app lib).

Notes

  • No behavioral changes beyond tests and deprecation comments.
  • Type-check and lint pass locally:
    • pnpm run lint (clean)
    • pnpm run lint:tsc (clean after building shared via pnpm --filter shared build)

Commit

  • test(webhook): update issues payload tests for expanded repository/sender schema; deprecate initializeWorkflowRun

Follow-up: migrate more workflows to StorageAdapter

What changed

  • Migrated run creation to StorageAdapter for key workflows so new runs persist Repository/User attribution:
    • lib/workflows/autoResolveIssue.ts
    • lib/workflows/commentOnIssue.ts
    • lib/workflows/alignmentCheck.ts (run creation delayed until issueNumber is resolved)
    • lib/workflows/createDependentPR.ts (StorageAdapter when a linked issue exists; legacy fallback otherwise)
  • Worker orchestrator now creates the WorkflowRun (via StorageAdapter) before invoking the shared usecase:
    • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
  • Shared usecase updated to rely on orchestrator for run creation (no longer calls initializeWorkflowRun):
    • shared/src/usecases/workflows/autoResolveIssue.ts

Implementation notes

  • StorageAdapter.workflow.run.create now used with full repository metadata and actor attribution (user placeholder { type: 'user', userId: 'system' }).
  • Owner login normalized from repository.owner.login with fallback to the repoFullName owner segment.
  • alignmentCheck creates the run only after determining the issue number (via linked issues or provided param) to satisfy the new schema.
  • createDependentPR initializes via StorageAdapter when a linked issue is present; if none is available, it falls back to the legacy initializeWorkflowRun to avoid dropping events while we iterate on optional issue linking.

Verification

  • Type-check: pnpm run lint:tsc (clean)
  • Lint: pnpm run lint (only import-order warnings; no errors)
  • Tests: pnpm run test:node and pnpm run test:components (all passing)

Next steps

  • We can migrate the remaining PR-oriented workflows (e.g., identifyPRGoal, reviewPullRequest, resolveMergeConflicts) once we finalize the approach for runs where a linked issue may not yet be determinable at creation time. That will let us remove remaining initializeWorkflowRun usages entirely.

Addresses

  • Partially addresses reviewer feedback to migrate off initializeWorkflowRun (GC-004).

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced GitHub webhook route security with comprehensive validation, signature verification, and event routing for issues, pull requests, installations, and repository events.
  • Documentation

    • Added Neo4j integration test setup documentation with environment configuration guidance and test database instructions.
  • Tests

    • Expanded webhook route test coverage with security, validation, and handler routing scenarios. Added comprehensive Neo4j integration test suite for workflow runs.

✏️ Tip: You can customize this high-level summary in your review settings.

…Adapter and update webhook types\n\n- Expand GitHub Issues webhook payload schema to include full repository and sender metadata\n- Replace initializeWorkflowRun with StorageAdapter.workflow.run.create in resolveIssue workflow\n- Add actor attribution (webhook/user) and pass full repository metadata to storage\n- Pass webhook context from issue label handler (sender id/login, installation id)\n- Use neo4jDs singleton in worker orchestrator and explicit READ sessions for settings\n- Update action files to use neo4jDs.getSession('READ') consistently\n- Remove deprecated linkToIssue from Neo4j repositories; mark listWorkflowRuns as @deprecated\n\nNotes:\n- Visibility is normalized to PUBLIC/PRIVATE/INTERNAL\n- Prepared for optional commit attribution support via StorageAdapter (not yet provided here)
@issuetopr-dev issuetopr-dev bot added the AI generated AI-generated Pull Requests label Jan 2, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 2, 2026

Walkthrough

This PR refactors workflow run persistence by introducing a modular StorageAdapter with separate attachment operations, removes legacy linkToIssue, adds comprehensive Neo4j integration tests with dedicated test database configuration, expands webhook payload schemas, and deprecates old workflow service functions in favor of the new StorageAdapter-based approach.

Changes

Cohort / File(s) Summary
Test Configuration & Infrastructure
.gitignore, __tests__/.env.example, __tests__/README.md
Added exception rule for .env.example, created test database environment template with Neo4j credentials, and documented Neo4j integration test setup including database configuration and pnpm test command
Neo4j Integration Tests
__tests__/shared/adapters/neo4j/testUtils.ts, __tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts, __tests__/shared/adapters/neo4j/queries.neo4j.test.ts, __tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts
Enhanced test utilities with dotenv loading and expanded Cypher cleanup logic for orphaned nodes; updated test setup documentation; added 400+ lines of comprehensive workflow run integration tests covering creation, attachment, event chaining, and retrieval
Webhook Tests
__tests__/api/webhook/github.route.test.ts, __tests__/api/webhook/github/route.test.ts
Deleted legacy route test file; added comprehensive new 640-line test suite covering security validation, event routing (issues, pull requests, installation, repository), and webhook signature verification
StorageAdapter & Neo4j Queries
shared/src/adapters/neo4j/StorageAdapter.ts, shared/src/adapters/neo4j/queries/workflowRuns/*
Introduced Neo4jCtx for context-driven operations; refactored workflow creation to use separate attachment operations (attachRepository, attachIssue, attachActor, attachCommit); added new query modules for creating, attaching, and retrieving workflow runs; exposed events.list API
Neo4j Type Definitions
shared/src/adapters/neo4j/types.ts, shared/src/entities/Commit.ts, shared/src/entities/WorkflowRun.ts, shared/src/entities/Queue.ts
Added workflowRunStateSchema and githubUserSchema; made optional fields: user.joinDate, repository.id/nodeId, commit.message and metadata fields; introduced WorkflowRunTypes discriminated union; added QueueEnum type alias
Legacy Function Removals & Deprecations
lib/neo4j/repositories/workflowRun.ts, lib/neo4j/services/workflow.ts, lib/workflows/resolveIssue.ts, shared/src/lib/neo4j/services/workflow.ts
Removed linkToIssue function and its int import; added @deprecated JSDoc to initializeWorkflowRun and listWorkflowRuns directing to StorageAdapter equivalents; marked resolveIssue as deprecated
Webhook Payload & Handler Updates
lib/webhook/github/types.ts, lib/webhook/github/handlers/issue/label.resolve.handler.ts
Expanded IssuesPayloadSchema.repository to include id, node_id, name, owner.login, and optional default_branch/visibility/has_issues; expanded sender to include id; updated session acquisition to use READ mode
Port & Storage Interface Definitions
shared/src/ports/db/index.ts
Completely reworked DatabaseStorage shape to support modular create flow; introduced Target, WorkflowRunConfig, WorkflowRunTrigger, and attachment interfaces; refactored CreateWorkflowRunInput with optional/typed fields; expanded WorkflowRunHandle with add/attach capabilities
Use Case & Orchestrator Updates
shared/src/usecases/workflows/autoResolveIssue.ts, apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
Updated autoResolveIssue to use storage.workflow.run.create instead of legacy initializeWorkflowRun; changed workflow type to "resolveIssue" and included target/actor/config payloads; orchestrator now uses shared neo4jDs and StorageAdapter instance instead of creating data source inline

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant SA as StorageAdapter
    participant Query as Neo4j Queries
    participant DB as Neo4j Database
    
    Caller->>SA: workflow.run.create(input)
    activate SA
    SA->>Query: createWorkflowRun(tx, core params)
    activate Query
    Query->>DB: CREATE WorkflowRun node
    DB-->>Query: new WorkflowRun
    Query-->>SA: CreateWorkflowRunResult
    deactivate Query
    
    alt Has Attachments
        SA->>Query: attachRepository(tx, repo)
        activate Query
        Query->>DB: MERGE Repository, link BASED_ON_REPO
        DB-->>Query: success
        Query-->>SA: AttachRepositoryResult
        deactivate Query
        
        SA->>Query: attachIssue(tx, issue)
        activate Query
        Query->>DB: MERGE Issue, link BASED_ON_ISSUE
        DB-->>Query: success
        Query-->>SA: AttachIssueResult
        deactivate Query
        
        SA->>Query: attachActor(tx, actor)
        activate Query
        Query->>DB: MERGE Actor, link INITIATED_BY
        DB-->>Query: success
        Query-->>SA: AttachActorResult
        deactivate Query
        
        SA->>Query: attachCommit(tx, commit)
        activate Query
        Query->>DB: MERGE Commit, link BASED_ON_COMMIT
        DB-->>Query: success
        Query-->>SA: AttachCommitResult
        deactivate Query
    end
    
    SA->>Query: getWorkflowRunById(tx, runId)
    activate Query
    Query->>DB: MATCH complete workflow graph
    DB-->>Query: GetWorkflowRunByIdResult
    Query-->>SA: mapped WorkflowRun
    deactivate Query
    
    SA-->>Caller: WorkflowRunHandle with run + add/attach APIs
    deactivate SA
Loading

Possibly related issues

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

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

@youngchingjui
Copy link
Owner

Let's make sure we also have test coverage of these code changes using jest --coverage.

@youngchingjui
Copy link
Owner

As we're migrating away from some functions, such as initializeWorkflowRun, let's mark them as @deprecated with directions on which new functions to use using jsdoc.

@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 2, 2026 15:22 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 2, 2026 15:22 Inactive
…nder schema; deprecate initializeWorkflowRun\n\n- Update webhook route tests to include required repository.id/node_id/name/owner.login and sender.id fields per new IssuesPayloadSchema\n- Add JSDoc @deprecated to initializeWorkflowRun with guidance to use StorageAdapter.workflow.run.create\n- Mark listWorkflowRuns as @deprecated in shared services for consistency\n\nThis addresses review feedback to ensure coverage reflects the schema change and to annotate deprecated functions with migration paths.
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 2, 2026 17:10 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 2, 2026 17:10 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 2, 2026 17:19 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 2, 2026 17:19 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 2, 2026 17:26 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 2, 2026 17:26 Inactive
@youngchingjui
Copy link
Owner

there's still quite a few workflows that are using the deprecated initializeWorkflowRun. We'll need to completely migrate away from this so we can start to record the right data using the new schemas.

@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 5, 2026 00:42 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 5, 2026 00:42 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 8, 2026 01:23 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 8, 2026 01:23 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 8, 2026 01:27 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 8, 2026 01:27 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 8, 2026 01:54 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 8, 2026 01:54 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime January 9, 2026 02:17 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook January 9, 2026 02:17 Inactive
@youngchingjui
Copy link
Owner

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@youngchingjui youngchingjui merged commit 41b8544 into main Jan 9, 2026
5 checks passed
@youngchingjui youngchingjui deleted the feature/workflows-repo-user-attribution-nodes branch January 9, 2026 02:26
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

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

⚠️ Outside diff range comments (2)
shared/src/adapters/neo4j/types.ts (1)

113-129: Risk: Making immutable identifiers optional undermines data integrity.

Lines 118-119 make id and nodeId optional, but the comment on line 115 explicitly states these are "Immutable identifiers." Making unique identifiers optional could lead to:

  1. Repository nodes without stable identifiers, making future matching/merging unreliable
  2. Potential duplicate nodes for the same repository
  3. Difficulty in enforcing uniqueness constraints

If repositories are created without these IDs initially, how will they be matched and merged when the IDs become available later? Consider requiring at least one immutable identifier at creation time.

💡 Suggested approach

If progressive attachment is needed, consider requiring fullName as the minimum identifier (since it's marked required) and making only nodeId optional. The numeric id should be required when available from GitHub API responses.

Alternatively, document the matching/merging strategy for repositories created without immutable identifiers.

shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.ts (1)

21-47: state can be null but TS type says it can’t.

Because the workflowState event match is optional, latestWorkflowState (and thus latestWorkflowState.state) can be null. But ListForRepoResult.state is typed as WorkflowRunState (non-null).

Proposed fix (default at query level)
-  RETURN w, userActor, webhookEvent, webhookSender, latestWorkflowState.state AS state, i, r, c
+  RETURN w, userActor, webhookEvent, webhookSender,
+         coalesce(latestWorkflowState.state, 'pending') AS state,
+         i, r, c
🤖 Fix all issues with AI agents
In @__tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts:
- Around line 132-145: The describe block name is outdated
("workflow.run.listEvents") and should match the API under test; update the
describe string to "workflow.events.list" so it reflects the calls to
adapter.workflow.events.list in the tests (locate the describe that wraps the
two it(...) cases and change its title to "workflow.events.list").

In @__tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts:
- Around line 29-46: The cleanup code is deleting Repository nodes by repo.id
but the tests create repos with fullName like "test/prog-repo-1", so change the
cleanup to delete by repo.fullName (e.g., MATCH (repo:Repository) WHERE
repo.fullName IN $repoFullNames DETACH DELETE repo) and pass
TEST_DATA.repositories as the $repoFullNames parameter, or alternatively update
TEST_DATA.repositories to contain the actual repo id values created by the
tests; apply the same fix to the other cleanup locations referenced (the other
uses of repo.id IN $repoIds).
- Around line 148-191: The test asserts handle.run.state === "pending" even
though WorkflowRun.state is event-sourced and not stored on the node; remove or
replace that brittle assertion: either drop the state check entirely in the
"Minimal workflow run creation" test (the remaining id/type checks and
relationship counts suffice) or fetch the canonical computed view (e.g., via
adapter.workflow.run.getById or the event-sourced projection) and assert its
computed state there; update references to handle.run.state in this test
accordingly so you don't rely on a non-persisted field.
- Around line 338-379: The test currently only checks events.length but not
order; update the assertion after calling
adapter.workflow.events.list(handle.run.id) to verify sequence by inspecting the
returned events array (from adapter.workflow.events.list) for ordering—e.g., map
to event payload content or order/timestamp fields and assert that the sequence
equals ["Event 1","Event 2","Event 3"] (or assert events[0].payload.content ===
"Event 1", events[1].payload.content === "Event 2", events[2].payload.content
=== "Event 3"). Ensure you reference the returned events from
adapter.workflow.events.list(handle.run.id) and keep the cleanup block
unchanged.

In @lib/webhook/github/handlers/issue/label.resolve.handler.ts:
- Line 32: labelerId is being extracted from payload but never used; decide
whether it should be passed into resolveIssue or added to the webhook actor
context for StorageAdapter workflow runs and implement accordingly. If labelerId
should be actor attribution, include it in the call that starts the workflow
(e.g., pass an actor or metadata object containing labelerId into resolveIssue
invocation or into the context passed to StorageAdapter.workflow.run.create) so
downstream code can attribute actions; if it is not needed yet, remove the const
labelerId = payload.sender?.id extraction to avoid dead code. Ensure you update
any call sites that construct the webhook context or resolveIssue parameters to
include labelerId under a clear key like actorId/labelerId.

In @shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts:
- Around line 148-152: Wrap the `default` switch clause in a block to satisfy
the Biome `noSwitchDeclarations` rule: replace the current `default:` section
with `default: { ... }` and move the `_exhaustive` declaration and the `throw
new Error(\`Unknown event type: ${(_exhaustive as AnyEvent).type}\`)` inside
that block so they are not declared directly in the switch scope; ensure you
keep the same `_exhaustive` symbol and `AnyEvent` cast and close the block.

In @shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts:
- Around line 8-33: Change the CREATE of the event node to a MERGE keyed by id
to make creation idempotent: replace the CREATE (newEvent:Event { ... }) with
MERGE (newEvent:Event { id: $eventId }) and move property assignments into an ON
CREATE SET block (set newEvent.type, newEvent.createdAt = datetime($createdAt),
and newEvent.content) so retries don't create duplicate Event nodes or reassign
timestamps; ensure all references (newEvent, lastEvent, wr) and the RETURN still
use the merged newEvent. Also apply the same MERGE + ON CREATE SET pattern to
the other identical event-creation block mentioned in the diff.

In @shared/src/adapters/neo4j/queries/workflowRuns/attachActor.ts:
- Around line 12-14: The queries USER_ACTOR_QUERY, GITHUB_USER_ACTOR_QUERY and
WEBHOOK_ACTOR_QUERY in attachActor only delete their own relationship type,
leaving stale actor relations; update each query so the pre-creation cleanup
OPTIONAL MATCH captures both INITIATED_BY and TRIGGERED_BY (e.g., OPTIONAL MATCH
(wr)-[oldRel:INITIATED_BY|TRIGGERED_BY]->() ) and DELETE oldRel so any existing
actor relationship is removed before creating the new one.

In @shared/src/adapters/neo4j/queries/workflowRuns/attachCommit.ts:
- Around line 8-21: The MERGE for Commit only sets commit.nodeId and
commit.message under ON CREATE so later attachCommit calls can't backfill
missing metadata; modify the query in attachCommit to always set or coalesce
these properties after the MERGE (e.g., add a SET clause that assigns
commit.nodeId = coalesce(commit.nodeId, $commitNodeId) and commit.message =
coalesce(commit.message, $commitMessage) or conditionally set when params are
not null) so subsequent calls update missing fields; apply the same change to
the other similar block referenced (lines 36-45).

In @shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.mapper.ts:
- Around line 15-21: The mapCreateWorkflowRunResult function currently declares
params but never uses it and accesses result.records[0] without checking for an
empty result; remove the unused params parameter (or document its intended
future use) and add a defensive null/length check on result.records before
accessing index 0 (e.g., ensure a record exists and throw or return a clear
error if not), then safely obtain wrNode from the validated record and call
workflowRunSchema.parse on its properties; ensure any thrown error includes
context about the createWorkflowRun query for easier debugging.

In @shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts:
- Around line 15-23: The CREATE of WorkflowRun is not idempotent; change the
Cypher in QUERY (and the other occurrence around the second block) to MERGE on
wr by id ($runId) and use ON CREATE SET to assign properties like type,
postToGithub, and createdAt so they are set only when a new node is created, and
use ON MATCH SET if you want to update mutable fields on duplicate deliveries;
update the query constants referenced (e.g., QUERY and the similar query at
lines 46-55) accordingly.

In @shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts:
- Around line 26-76: The mapActor function should not coerce missing
webhookEvent.action to an empty string; update the webhook branch in mapActor to
set action to webhookEvent.action (which will be undefined when absent) instead
of using `?? ""`, and ensure the WorkflowRunActor type/interface accepts an
optional action so no runtime or type errors occur.
- Around line 98-131: The commit mapping uses a non-null assertion repository!
which will throw if a commitNode exists but repoNode is missing; update the
commit mapping in getWorkflowRunById mapper to avoid the non-null assertion by
making commit.repository optional or deriving a repository value from the commit
properties (e.g., use parsed.repository/repoFullName from commitSchema if
present) and only set repository when you have valid data, or alternatively
ensure upstream query always returns the repo when a commit exists (fetch
(commit)-[:IN_REPOSITORY]->(repo)) if domain requires a repository.
- Around line 95-143: The mapper is incorrectly deriving state from wr.state
(workflowRun node) which is not stored there; update the getWorkflowRunById
Cypher to also MATCH the latest workflowState Event (same pattern used in
listForRepo/listForIssue/listByUser: `(w)-[:STARTS_WITH|NEXT*]->(e:Event {type:
'workflowState'})` and return it as latestWorkflowState) and pass that node into
this mapper; then change the mapping logic to use
latestWorkflowState.properties.state (or its parsed event via the event schema)
instead of wr.state (so replace `state: wr.state ?? "pending"` with the derived
`state` from latestWorkflowState, falling back to "pending" if missing). Ensure
the mapper references the returned latestWorkflowState symbol and parses it
consistently with other mappers.
🧹 Nitpick comments (10)
shared/src/entities/Queue.ts (1)

3-4: Consider resolving the architectural placement questions.

The TODOs raise valid concerns about whether queue definitions belong closer to their consumers (neo4j adapters) or with related channel definitions. Since this file is now part of the public entity surface, clarifying its placement would improve maintainability.

Consider:

  • If queues are tightly coupled to Neo4j operations, moving to shared/src/adapters/neo4j/ might improve cohesion
  • If queues are a general messaging concept, keeping them separate or in a shared/src/entities/messaging/ namespace might be clearer
  • Document the decision to help future contributors understand the rationale
__tests__/api/webhook/github/route.test.ts (1)

245-276: Consider adding test coverage for optional repository fields.

The test payloads currently omit the optional repository fields (default_branch, visibility, has_issues). While the schema correctly handles this, adding test cases that include these fields would improve coverage and document their expected behavior in the webhook flow.

📝 Example test case for optional fields
it("routes i2pr resolve issue label with full repository metadata", async () => {
  const payload = {
    action: "labeled",
    label: { name: "i2pr: resolve issue" },
    repository: {
      id: 123,
      node_id: "R_kgDOTest",
      full_name: "octo-org/octo-repo",
      name: "octo-repo",
      owner: { login: "octo-org" },
      default_branch: "main",
      visibility: "public" as const,
      has_issues: true,
    },
    issue: { number: 42 },
    sender: { id: 1001, login: "octocat" },
    installation: { id: 9876 },
  }
  const mockRequest = createSignedRequest(payload, "issues")

  const response = await POST(mockRequest)

  expect(response.status).toBe(200)
  expect(handleIssueLabelAutoResolve).toHaveBeenCalledWith(
    expect.objectContaining({
      payload: expect.objectContaining({
        repository: expect.objectContaining({
          default_branch: "main",
          visibility: "public",
          has_issues: true,
        }),
      }),
    })
  )
})
shared/src/adapters/neo4j/queries/workflowRuns/listByUser.ts (2)

1-14: Consider completing the type-only import migration for consistency.

While QueryResult and the domain types (Commit, Issue, User, etc.) were correctly converted to type-only imports, Integer, ManagedTransaction, and Node are also only used in type positions and could be converted for consistency.

♻️ Optional: Complete type-only import migration
 import {
-  Integer,
-  ManagedTransaction,
-  Node,
+  type Integer,
+  type ManagedTransaction,
+  type Node,
   type QueryResult,
 } from "neo4j-driver"

22-25: Verification complete: all downstream consumers properly updated.

The removal of the LINKED_GITHUB_USER relationship and gh field is safe:

  • No references to the removed gh field exist in the codebase
  • The ListByUserResult interface (lines 32-39) and query correctly omit gh
  • The mapper and tests use only the new result shape without issues
  • Existing Neo4j data is unaffected (removal is query-only)

Consider refactoring: shared/src/adapters/neo4j/queries/workflowRuns/index.ts is a barrel file re-exporting symbols from listByUser and listByUser.mapper. Import from original module paths directly to maintain clearer dependency graphs and better tree-shaking.

shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.ts (1)

1-13: Consider completing the type-only import migration for consistency.

Similar to listByUser.ts, Integer, ManagedTransaction, and Node are only used in type positions and could be converted to type-only imports for consistency with the other type-only imports in this file.

♻️ Optional: Complete type-only import migration
 import {
-  Integer,
-  ManagedTransaction,
-  Node,
+  type Integer,
+  type ManagedTransaction,
+  type Node,
   type QueryResult,
 } from "neo4j-driver"
__tests__/shared/adapters/neo4j/testUtils.ts (1)

138-142: Minor: Label parameter interpolation in Cypher.

While this is a test utility and label is controlled internally, directly interpolating into Cypher queries is a pattern to avoid. Consider using a parameterized approach or validating the label against an allowlist.

Suggested improvement
+const VALID_LABELS = ["WorkflowRun", "Repository", "Issue", "Commit", "Actor"] as const
+
 export async function getNodeCount(
   ds: Neo4jDataSource,
   label: string
 ): Promise<number> {
+  if (!VALID_LABELS.includes(label as typeof VALID_LABELS[number])) {
+    throw new Error(`Invalid label: ${label}`)
+  }
   const session = ds.getSession("READ")
shared/src/adapters/neo4j/queries/workflowRuns/attachRepository.ts (1)

15-18: Inconsistent update strategy for derived vs source fields.

The ON MATCH block updates fullName unconditionally (line 15) but uses coalesce for owner and name (lines 17-18). Since fullName is derived from owner and name (line 55), this creates a potential inconsistency: if the incoming repoOwner or repoName are empty/null but protected by coalesce, the repository will retain its old owner/name but get a freshly computed fullName that doesn't match them.

Consider one of these approaches:

  • Option 1 (recommended): Also apply coalesce to fullName for consistency: repo.fullName = coalesce($repoFullName, repo.fullName)
  • Option 2: Remove coalesce from owner and name if the intent is to always overwrite with fresh webhook data.
Proposed fix: Apply coalesce to fullName
  ON MATCH SET repo.fullName = $repoFullName,
+              repo.fullName = coalesce($repoFullName, repo.fullName),
               repo.githubInstallationId = coalesce($repoGithubInstallationId, repo.githubInstallationId),
-              repo.owner = coalesce($repoOwner, repo.owner),
-              repo.name = coalesce($repoName, repo.name)
+              repo.owner = $repoOwner,
+              repo.name = $repoName

(Choose either approach depending on your update semantics)

shared/src/adapters/neo4j/StorageAdapter.ts (1)

167-176: Consider clarifying the error message for partial repository data.

The error message on line 173-175 suggests using handle.attach.repository() instead, but this function is called by both handle.attach.target() and internally by runCreate. The message could be more helpful by explaining that all required fields (id, owner, name) must be provided.

Suggested improvement
  throw new Error(
-   "Repository attachment via target requires repository.id + owner + name - use handle.attach.repository() instead"
+   "Repository attachment requires repository.id, owner, and name to be provided. " +
+   "Either supply all fields in target.repository or use handle.attach.repository() with complete data."
  )
shared/src/adapters/neo4j/queries/workflowRuns/index.ts (1)

1-18: Avoid barrel file pattern per coding guidelines.

This file acts as a barrel file whose sole purpose is to re-export symbols from multiple modules. As per the coding guidelines, prefer importing from original module paths to maintain clear dependency graphs and enable better tree-shaking.

Consider removing this index file and updating imports throughout the codebase to reference the original module paths directly (e.g., import { createWorkflowRun } from "@/shared/adapters/neo4j/queries/workflowRuns/createWorkflowRun" instead of import { createWorkflowRun } from "@/shared/adapters/neo4j/queries/workflowRuns").

As per coding guidelines.

shared/src/ports/db/index.ts (1)

36-40: Consider stronger typing for payload field.

The payload field is typed as unknown, which bypasses TypeScript's type checking and could allow invalid event payloads to pass through. Since AllEvents is a discriminated union with specific payload structures for each event type, consider using a more specific type or implementing runtime validation.

Given that the PR already uses Zod schemas for webhook payload validation (per PR objectives), consider creating Zod schemas for these input types as well to ensure type safety at both compile-time and runtime.

Possible approaches

Option 1: Use a discriminated union approach:

export type WorkflowEventInput = {
  [K in AllEvents["type"]]: {
    type: K
    payload: Extract<AllEvents, { type: K }>["payload"]
    createdAt?: string
  }
}[AllEvents["type"]]

Option 2: Create Zod schemas for validation (aligning with webhook payload patterns in the PR):

export const WorkflowEventInputSchema = z.object({
  type: z.enum([/* event types */]),
  payload: z.unknown(), // then validate based on type
  createdAt: z.string().optional(),
})
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a437c7 and 217ab10.

📒 Files selected for processing (42)
  • .gitignore
  • __tests__/.env.example
  • __tests__/README.md
  • __tests__/api/webhook/github.route.test.ts
  • __tests__/api/webhook/github/route.test.ts
  • __tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts
  • __tests__/shared/adapters/neo4j/queries.neo4j.test.ts
  • __tests__/shared/adapters/neo4j/testUtils.ts
  • __tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts
  • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
  • lib/neo4j/repositories/workflowRun.ts
  • lib/neo4j/services/workflow.ts
  • lib/webhook/github/handlers/issue/label.resolve.handler.ts
  • lib/webhook/github/types.ts
  • lib/workflows/resolveIssue.ts
  • shared/src/adapters/neo4j/StorageAdapter.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachActor.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachCommit.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachRepository.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/index.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.ts
  • shared/src/adapters/neo4j/types.ts
  • shared/src/entities/Commit.ts
  • shared/src/entities/Queue.ts
  • shared/src/entities/WorkflowRun.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • shared/src/ports/db/index.ts
  • shared/src/usecases/workflows/autoResolveIssue.ts
💤 Files with no reviewable changes (1)
  • tests/api/webhook/github.route.test.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx,mts,mjs}

📄 CodeRabbit inference engine (.cursor/rules/code-structure.mdc)

Avoid barrel files: do not create files whose sole purpose is to re-export symbols from multiple modules; prefer importing from original module paths to maintain clear dependency graphs and better tree-shaking

Files:

  • shared/src/entities/Commit.ts
  • __tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts
  • shared/src/entities/WorkflowRun.ts
  • lib/neo4j/repositories/workflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.ts
  • __tests__/api/webhook/github/route.test.ts
  • __tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachCommit.ts
  • __tests__/shared/adapters/neo4j/testUtils.ts
  • lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • lib/webhook/github/handlers/issue/label.resolve.handler.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachRepository.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.mapper.ts
  • shared/src/ports/db/index.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.mapper.ts
  • shared/src/entities/Queue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • lib/webhook/github/types.ts
  • shared/src/adapters/neo4j/StorageAdapter.ts
  • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
  • __tests__/shared/adapters/neo4j/queries.neo4j.test.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/index.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • lib/workflows/resolveIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachIssue.ts
  • shared/src/adapters/neo4j/types.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.ts
  • shared/src/usecases/workflows/autoResolveIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachActor.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1241
File: app/api/webhook/github/route.ts:85-90
Timestamp: 2025-09-12T01:44:51.070Z
Learning: In the webhook handlers at app/api/webhook/github/route.ts, runWithInstallationId is intentionally not awaited to allow fire-and-forget background processing of long-running workflows, avoiding webhook timeouts. The team plans to migrate these to proper queues/workers in a separate issue later.
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1282
File: lib/adapters/PersistingEventBusAdapter.ts:55-58
Timestamp: 2025-09-22T09:28:41.886Z
Learning: The project is migrating away from neo4j/services/event adapters, so issues in files like lib/adapters/PersistingEventBusAdapter.ts that use these adapters may be temporary and lower priority.
📚 Learning: 2026-01-02T09:21:42.011Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1450
File: shared/src/adapters/neo4j/types.ts:12-17
Timestamp: 2026-01-02T09:21:42.011Z
Learning: In the Neo4j data model at shared/src/adapters/neo4j/types.ts, the WorkflowRun node intentionally does not include a `state` field. The workflow run state is derived from the chain of events rather than being stored directly on the WorkflowRun node, following an event sourcing pattern where state is computed by replaying events.

Applied to files:

  • __tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts
  • shared/src/entities/WorkflowRun.ts
  • lib/neo4j/repositories/workflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.ts
  • __tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachCommit.ts
  • lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachRepository.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.mapper.ts
  • shared/src/ports/db/index.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.mapper.ts
  • shared/src/entities/Queue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • shared/src/adapters/neo4j/StorageAdapter.ts
  • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/index.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachIssue.ts
  • shared/src/adapters/neo4j/types.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.ts
  • shared/src/usecases/workflows/autoResolveIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/attachActor.ts
📚 Learning: 2025-09-22T09:28:41.886Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1282
File: lib/adapters/PersistingEventBusAdapter.ts:55-58
Timestamp: 2025-09-22T09:28:41.886Z
Learning: The project is migrating away from neo4j/services/event adapters, so issues in files like lib/adapters/PersistingEventBusAdapter.ts that use these adapters may be temporary and lower priority.

Applied to files:

  • __tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts
  • lib/neo4j/repositories/workflowRun.ts
  • __tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • shared/src/adapters/neo4j/StorageAdapter.ts
  • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
  • __tests__/shared/adapters/neo4j/queries.neo4j.test.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/index.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/types.ts
📚 Learning: 2025-09-22T09:32:04.764Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1282
File: lib/types/db/neo4j.ts:89-91
Timestamp: 2025-09-22T09:32:04.764Z
Learning: The new WorkflowStateEventSchema in shared/src/entities/events/WorkflowEvent.ts is built from BaseFields (id, timestamp) and does not contain a workflowId field, so DB-level schemas should not attempt to omit workflowId when using this schema.

Applied to files:

  • shared/src/entities/WorkflowRun.ts
  • lib/neo4j/repositories/workflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.mapper.ts
  • shared/src/ports/db/index.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/index.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/types.ts
  • shared/src/usecases/workflows/autoResolveIssue.ts
📚 Learning: 2025-09-22T09:32:04.764Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1282
File: lib/types/db/neo4j.ts:89-91
Timestamp: 2025-09-22T09:32:04.764Z
Learning: The new WorkflowStateEventSchema in shared/src/entities/events/WorkflowEvent.ts does not contain a workflowId field, so DB-level schemas should not attempt to omit it when using this schema.

Applied to files:

  • shared/src/entities/WorkflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.mapper.ts
  • shared/src/ports/db/index.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/index.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/types.ts
📚 Learning: 2025-09-22T09:34:43.580Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1282
File: lib/neo4j/convert.ts:69-74
Timestamp: 2025-09-22T09:34:43.580Z
Learning: The file lib/neo4j/convert.ts is being moved away from/deprecated, so fixes in this file may not be worth implementing since it will be replaced.

Applied to files:

  • lib/neo4j/repositories/workflowRun.ts
  • lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • shared/src/adapters/neo4j/StorageAdapter.ts
  • __tests__/shared/adapters/neo4j/queries.neo4j.test.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/types.ts
📚 Learning: 2025-09-22T09:33:51.194Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1282
File: lib/neo4j/services/event.ts:365-370
Timestamp: 2025-09-22T09:33:51.194Z
Learning: The file lib/neo4j/services/event.ts is planned for migration/deprecation, so minor fixes may not be worth implementing since the file will be replaced.

Applied to files:

  • lib/neo4j/repositories/workflowRun.ts
  • __tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • __tests__/shared/adapters/neo4j/queries.neo4j.test.ts
  • shared/src/lib/neo4j/services/workflow.ts
  • shared/src/adapters/neo4j/types.ts
📚 Learning: 2025-09-12T01:44:51.070Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1241
File: app/api/webhook/github/route.ts:85-90
Timestamp: 2025-09-12T01:44:51.070Z
Learning: In the webhook handlers at app/api/webhook/github/route.ts, runWithInstallationId is intentionally not awaited to allow fire-and-forget background processing of long-running workflows, avoiding webhook timeouts. The team plans to migrate these to proper queues/workers in a separate issue later.

Applied to files:

  • __tests__/api/webhook/github/route.test.ts
  • shared/src/ports/db/index.ts
  • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
  • shared/src/usecases/workflows/autoResolveIssue.ts
📚 Learning: 2026-01-02T09:20:57.476Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1450
File: shared/src/adapters/neo4j/types.ts:272-283
Timestamp: 2026-01-02T09:20:57.476Z
Learning: The plan schema and llmResponseWithPlanSchema in shared/src/adapters/neo4j/types.ts are being migrated away/deprecated, so schema inconsistencies related to plan support do not need to be fixed.

Applied to files:

  • __tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listByUser.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.mapper.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForIssue.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listEvents.ts
  • shared/src/adapters/neo4j/StorageAdapter.ts
  • __tests__/shared/adapters/neo4j/queries.neo4j.test.ts
  • shared/src/adapters/neo4j/types.ts
  • shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.mapper.ts
📚 Learning: 2025-09-22T09:32:07.272Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1282
File: shared/src/entities/events/WorkflowEvent.ts:15-20
Timestamp: 2025-09-22T09:32:07.272Z
Learning: For workflow.started events in shared/src/entities/events/WorkflowEvent.ts, the content field is intentionally optional, not required. The schema correctly uses z.string().optional().

Applied to files:

  • shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts
  • shared/src/adapters/neo4j/types.ts
📚 Learning: 2025-09-04T14:58:22.961Z
Learnt from: CR
Repo: youngchingjui/issue-to-pr PR: 0
File: .cursor/rules/env-management.mdc:0-0
Timestamp: 2025-09-04T14:58:22.961Z
Learning: Applies to .env : Never commit sensitive environment variables to the repository

Applied to files:

  • .gitignore
📚 Learning: 2025-09-10T06:22:04.642Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1223
File: components/RepositoryList.tsx:0-0
Timestamp: 2025-09-10T06:22:04.642Z
Learning: Repository links in RepositoryList.tsx should use repo.full_name to construct paths (e.g., `/${repo.full_name}/issues`) instead of manually combining owner and repository name, as this ensures correct routing for both personal and organization repositories.

Applied to files:

  • lib/webhook/github/types.ts
📚 Learning: 2025-09-11T09:11:14.319Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1214
File: lib/webhook.ts:184-189
Timestamp: 2025-09-11T09:11:14.319Z
Learning: The autoResolveIssue workflow in the codebase follows clean architecture principles with a two-argument signature: autoResolveIssue(params, ports), where ports contains auth and settings adapters.

Applied to files:

  • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
  • lib/workflows/resolveIssue.ts
  • shared/src/usecases/workflows/autoResolveIssue.ts
📚 Learning: 2025-09-11T09:11:14.319Z
Learnt from: youngchingjui
Repo: youngchingjui/issue-to-pr PR: 1214
File: lib/webhook.ts:184-189
Timestamp: 2025-09-11T09:11:14.319Z
Learning: The autoResolveIssue workflow signature is autoResolveIssue(params, ports) where params contains {issue, repository, jobId} and ports contains {auth: AuthReaderPort, settings: SettingsReaderPort, eventBus?: EventBusPort}.

Applied to files:

  • lib/workflows/resolveIssue.ts
  • shared/src/usecases/workflows/autoResolveIssue.ts
🧬 Code graph analysis (13)
__tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts (2)
__tests__/shared/adapters/neo4j/testUtils.ts (2)
  • createTestDataSource (23-41)
  • verifyConnection (46-57)
shared/src/adapters/neo4j/StorageAdapter.ts (1)
  • StorageAdapter (383-436)
shared/src/entities/WorkflowRun.ts (1)
shared/src/adapters/neo4j/types.ts (1)
  • WorkflowRun (322-322)
shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.ts (1)
shared/src/adapters/neo4j/types.ts (6)
  • WorkflowRun (322-322)
  • Repository (309-309)
  • Issue (304-304)
  • Commit (299-299)
  • GithubWebhookEvent (303-303)
  • GithubUser (302-302)
__tests__/api/webhook/github/route.test.ts (6)
lib/webhook/github/handlers/issue/label.autoResolveIssue.handler.ts (1)
  • handleIssueLabelAutoResolve (10-46)
lib/webhook/github/handlers/issue/label.resolve.handler.ts (1)
  • handleIssueLabelResolve (22-89)
lib/webhook/github/handlers/pullRequest/label.createDependentPR.handler.ts (1)
  • handlePullRequestLabelCreateDependentPR (8-40)
lib/webhook/github/handlers/pullRequest/closed.removeContainer.handler.ts (1)
  • handlePullRequestClosedRemoveContainer (8-52)
lib/webhook/github/handlers/installation/revalidateRepositoriesCache.handler.ts (1)
  • revalidateUserInstallationReposCache (12-17)
lib/webhook/github/handlers/repository/edited.revalidateRepoCache.handler.ts (1)
  • handleRepositoryEditedRevalidate (9-17)
shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts (3)
shared/src/adapters/neo4j/queries/workflowRuns/addEvent.ts (1)
  • AddEventResult (43-48)
shared/src/entities/events/index.ts (1)
  • AllEvents (6-6)
shared/src/adapters/neo4j/types.ts (2)
  • anyEventSchema (284-295)
  • AnyEvent (298-298)
shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts (3)
shared/src/entities/WorkflowRun.ts (1)
  • WorkflowRunActor (17-17)
shared/src/adapters/neo4j/types.ts (9)
  • githubUserSchema (46-52)
  • githubWebhookEventSchema (98-101)
  • genericWebhookEventSchema (105-111)
  • userSchema (39-43)
  • WorkflowRun (322-322)
  • workflowRunSchema (30-36)
  • repositorySchema (117-129)
  • issueSchema (160-170)
  • commitSchema (136-158)
shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.ts (1)
  • GetWorkflowRunByIdResult (36-44)
shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.mapper.ts (2)
shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts (2)
  • CreateWorkflowRunResult (38-40)
  • CreateWorkflowRunParams (32-36)
shared/src/adapters/neo4j/types.ts (2)
  • WorkflowRun (322-322)
  • workflowRunSchema (30-36)
shared/src/ports/db/index.ts (3)
shared/src/entities/WorkflowRun.ts (3)
  • WorkflowRunTypes (19-25)
  • WorkflowRunActor (17-17)
  • WorkflowRun (27-37)
shared/src/entities/events/index.ts (1)
  • AllEvents (6-6)
shared/src/adapters/neo4j/types.ts (1)
  • WorkflowRun (322-322)
shared/src/adapters/neo4j/StorageAdapter.ts (6)
shared/src/ports/db/index.ts (9)
  • CreateWorkflowRunInput (27-34)
  • WorkflowEventInput (36-40)
  • Target (8-21)
  • RepositoryAttachment (42-49)
  • IssueAttachment (51-54)
  • CommitAttachment (56-60)
  • WorkflowRunHandle (62-74)
  • DatabaseStorage (82-93)
  • WorkflowRunFilter (76-80)
shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts (2)
  • CreateWorkflowRunParams (32-36)
  • createWorkflowRun (46-55)
shared/src/adapters/neo4j/queries/workflowRuns/attachRepository.ts (1)
  • attachRepository (39-60)
shared/src/adapters/neo4j/queries/workflowRuns/attachIssue.ts (1)
  • attachIssue (27-36)
shared/src/adapters/neo4j/queries/workflowRuns/attachCommit.ts (1)
  • attachCommit (36-46)
shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts (1)
  • mapGetWorkflowRunById (81-144)
apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts (4)
apps/workers/workflow-workers/src/helper.ts (1)
  • getEnvVar (55-58)
shared/src/adapters/neo4j/repositories/SettingsReaderAdapter.ts (1)
  • makeSettingsReaderAdapter (28-54)
shared/src/auth.ts (1)
  • setAccessToken (11-13)
shared/src/adapters/neo4j/StorageAdapter.ts (1)
  • StorageAdapter (383-436)
shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.ts (1)
shared/src/adapters/neo4j/types.ts (3)
  • User (320-320)
  • GithubWebhookEvent (303-303)
  • GithubUser (302-302)
shared/src/usecases/workflows/autoResolveIssue.ts (4)
shared/src/ports/db/index.ts (1)
  • DatabaseStorage (82-93)
shared/src/ports/events/eventBus.ts (1)
  • EventBusPort (8-13)
apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts (1)
  • autoResolveIssue (46-121)
lib/workflows/autoResolveIssue.ts (1)
  • autoResolveIssue (42-225)
shared/src/adapters/neo4j/queries/workflowRuns/listForRepo.mapper.ts (2)
shared/src/entities/WorkflowRun.ts (1)
  • WorkflowRunActor (17-17)
shared/src/adapters/neo4j/types.ts (3)
  • githubWebhookEventSchema (98-101)
  • githubUserSchema (46-52)
  • userSchema (39-43)
🪛 Biome (2.1.2)
shared/src/adapters/neo4j/queries/workflowRuns/addEvent.mapper.ts

[error] 150-151: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

Comment on lines 132 to 145
describe("workflow.run.listEvents", () => {
it("should return empty array (placeholder implementation)", async () => {
const result = await adapter.workflow.run.listEvents("any-run-id")
const result = await adapter.workflow.events.list("any-run-id")
expect(result).toEqual([])
})

it("should accept any run id without error", async () => {
await expect(adapter.workflow.events.list("test-run-1")).resolves.toEqual(
[]
)
await expect(
adapter.workflow.run.listEvents("test-run-1")
).resolves.toEqual([])
await expect(
adapter.workflow.run.listEvents("non-existent")
adapter.workflow.events.list("non-existent")
).resolves.toEqual([])
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Describe block name doesn't match the API being tested.

The describe block is named "workflow.run.listEvents" but the tests now call adapter.workflow.events.list. Update the describe name to match the new API path.

Proposed fix
-  describe("workflow.run.listEvents", () => {
+  describe("workflow.events.list", () => {
     it("should return empty array (placeholder implementation)", async () => {
       const result = await adapter.workflow.events.list("any-run-id")
       expect(result).toEqual([])
📝 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
describe("workflow.run.listEvents", () => {
it("should return empty array (placeholder implementation)", async () => {
const result = await adapter.workflow.run.listEvents("any-run-id")
const result = await adapter.workflow.events.list("any-run-id")
expect(result).toEqual([])
})
it("should accept any run id without error", async () => {
await expect(adapter.workflow.events.list("test-run-1")).resolves.toEqual(
[]
)
await expect(
adapter.workflow.run.listEvents("test-run-1")
).resolves.toEqual([])
await expect(
adapter.workflow.run.listEvents("non-existent")
adapter.workflow.events.list("non-existent")
).resolves.toEqual([])
})
describe("workflow.events.list", () => {
it("should return empty array (placeholder implementation)", async () => {
const result = await adapter.workflow.events.list("any-run-id")
expect(result).toEqual([])
})
it("should accept any run id without error", async () => {
await expect(adapter.workflow.events.list("test-run-1")).resolves.toEqual(
[]
)
await expect(
adapter.workflow.events.list("non-existent")
).resolves.toEqual([])
})
🤖 Prompt for AI Agents
In @__tests__/shared/adapters/neo4j/StorageAdapter.neo4j.test.ts around lines
132 - 145, The describe block name is outdated ("workflow.run.listEvents") and
should match the API under test; update the describe string to
"workflow.events.list" so it reflects the calls to adapter.workflow.events.list
in the tests (locate the describe that wraps the two it(...) cases and change
its title to "workflow.events.list").

Comment on lines +29 to +46
// Hardcoded test data IDs for idempotent testing
const TEST_DATA = {
workflowRuns: [
"test-workflow-run-minimal",
"test-workflow-run-full",
"test-workflow-run-events",
"test-workflow-run-step-by-step",
"test-workflow-run-backward-compat",
],
repositories: ["test-prog-repo-1", "test-prog-repo-2"],
users: ["test-prog-user-1"],
githubUsers: ["test-prog-github-user-1"],
commits: ["test-prog-commit-1"],
issues: [
{ number: 42, repoFullName: "test/repo" },
{ number: 100, repoFullName: "test/prog-repo-2" },
],
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix test DB cleanup: repository deletion key doesn’t match what the tests create.

cleanupHardcodedTestData() deletes Repository by repo.id IN $repoIds (Line 88), but the tests create repos with id: 12345/67890 and fullName: test/prog-repo-*. TEST_DATA.repositories contains "test-prog-repo-1" etc., so repos likely won’t be deleted.

Proposed fix
-  repositories: ["test-prog-repo-1", "test-prog-repo-2"],
+  repositories: ["test/prog-repo-1", "test/prog-repo-2"],
-      // Delete repositories
+      // Delete repositories (keyed by fullName, not id)
       await session.run(
         `
         MATCH (repo:Repository)
-        WHERE repo.id IN $repoIds
+        WHERE repo.fullName IN $repoFullNames
         DETACH DELETE repo
       `,
-        { repoIds: TEST_DATA.repositories }
+        { repoFullNames: TEST_DATA.repositories }
       )

Also applies to: 84-92, 227-234, 280-287

🤖 Prompt for AI Agents
In @__tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts around lines 29 -
46, The cleanup code is deleting Repository nodes by repo.id but the tests
create repos with fullName like "test/prog-repo-1", so change the cleanup to
delete by repo.fullName (e.g., MATCH (repo:Repository) WHERE repo.fullName IN
$repoFullNames DETACH DELETE repo) and pass TEST_DATA.repositories as the
$repoFullNames parameter, or alternatively update TEST_DATA.repositories to
contain the actual repo id values created by the tests; apply the same fix to
the other cleanup locations referenced (the other uses of repo.id IN $repoIds).

Comment on lines +71 to +146
async function cleanupHardcodedTestData() {
const session = dataSource.getSession("WRITE")
try {
// Delete workflow runs
await session.run(
`
MATCH (wr:WorkflowRun)
WHERE wr.id IN $runIds
DETACH DELETE wr
`,
{ runIds: TEST_DATA.workflowRuns }
)

// Delete repositories
await session.run(
`
MATCH (repo:Repository)
WHERE repo.id IN $repoIds
DETACH DELETE repo
`,
{ repoIds: TEST_DATA.repositories }
)

// Delete users
await session.run(
`
MATCH (user:User)
WHERE user.id IN $userIds
DETACH DELETE user
`,
{ userIds: TEST_DATA.users }
)

// Delete GitHub users
await session.run(
`
MATCH (ghUser:GithubUser)
WHERE ghUser.id IN $ghUserIds
DETACH DELETE ghUser
`,
{ ghUserIds: TEST_DATA.githubUsers }
)

// Delete commits
await session.run(
`
MATCH (commit:Commit)
WHERE commit.sha IN $commitShas
DETACH DELETE commit
`,
{ commitShas: TEST_DATA.commits }
)

// Delete issues
for (const issue of TEST_DATA.issues) {
await session.run(
`
MATCH (issue:Issue {number: $number, repoFullName: $repoFullName})
DETACH DELETE issue
`,
{ number: int(issue.number), repoFullName: issue.repoFullName }
)
}

// Delete events
await session.run(
`
MATCH (event:Event)
WHERE event.id STARTS WITH 'test-prog-'
DETACH DELETE event
`
)
} finally {
await session.close()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent orphaned Event nodes: deleting only WorkflowRun leaves event chains behind.

DETACH DELETE wr removes relationships but not the connected Event nodes, so repeated runs will accumulate orphaned events. The current event cleanup (Line 136-142) relies on event.id STARTS WITH 'test-prog-', but handle.add.event() likely generates non-prefixed ids.

Proposed fix (delete run + attached events together)
-        await session.run(
-          `
-          MATCH (wr:WorkflowRun {id: $id})
-          DETACH DELETE wr
-        `,
-          { id: handle.run.id }
-        )
+        await session.run(
+          `
+          MATCH (wr:WorkflowRun {id: $id})
+          OPTIONAL MATCH (wr)-[:STARTS_WITH|NEXT*]->(e:Event)
+          DETACH DELETE wr, e
+        `,
+          { id: handle.run.id }
+        )

Apply similarly to the other per-test cleanups (Line 366-378, Line 398-409), and consider folding this into a shared helper to avoid drift.

Also applies to: 366-378, 398-409

Comment on lines +148 to +191
describe("Minimal workflow run creation", () => {
it("should create workflow run with only type", async () => {
const handle = await adapter.workflow.run.create({
id: TEST_DATA.workflowRuns[0],
type: "resolveIssue",
})

expect(handle.run.id).toBe(TEST_DATA.workflowRuns[0])
expect(handle.run.type).toBe("resolveIssue")
expect(handle.run.state).toBe("pending")

// Verify only WorkflowRun node exists, no relationships
const session = dataSource.getSession("READ")
try {
const result = await session.run(
`
MATCH (wr:WorkflowRun {id: $id})
RETURN
COUNT { (wr)-[:BASED_ON_REPOSITORY]->() } AS repoCount,
COUNT { (wr)-[:BASED_ON_ISSUE]->() } AS issueCount,
COUNT { (wr)-[:INITIATED_BY]->() } AS actorCount
`,
{ id: handle.run.id }
)

const record = result.records[0]
const repoCount = record?.get("repoCount")
const issueCount = record?.get("issueCount")
const actorCount = record?.get("actorCount")

// COUNT {} returns Neo4j Integer, convert to number
expect(
typeof repoCount === "number" ? repoCount : repoCount?.toNumber()
).toBe(0)
expect(
typeof issueCount === "number" ? issueCount : issueCount?.toNumber()
).toBe(0)
expect(
typeof actorCount === "number" ? actorCount : actorCount?.toNumber()
).toBe(0)
} finally {
await session.close()
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

handle.run.state assertion may be incompatible with event-sourced state.

The test asserts handle.run.state === "pending" (Line 157). If state is intended to be derived from workflowState events (rather than stored on WorkflowRun), this assertion is brittle. Based on learnings, the WorkflowRun node intentionally does not include state.

Safer assertion options
-      expect(handle.run.state).toBe("pending")
+      // If state is event-sourced, validate via events (or via getById if it computes state).
+      expect(handle.run.id).toBe(TEST_DATA.workflowRuns[0])

Or, if getById() is the canonical computed view:

+      const retrieved = await adapter.workflow.run.getById(handle.run.id)
+      expect(retrieved?.state).toBe("pending")

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @__tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts around lines 148
- 191, The test asserts handle.run.state === "pending" even though
WorkflowRun.state is event-sourced and not stored on the node; remove or replace
that brittle assertion: either drop the state check entirely in the "Minimal
workflow run creation" test (the remaining id/type checks and relationship
counts suffice) or fetch the canonical computed view (e.g., via
adapter.workflow.run.getById or the event-sourced projection) and assert its
computed state there; update references to handle.run.state in this test
accordingly so you don't rely on a non-persisted field.

Comment on lines +338 to +379
describe("Event chaining", () => {
it("should chain events in order via handle.add.event", async () => {
const handle = await adapter.workflow.run.create({
type: "resolveIssue",
})

// Add first event
await handle.add.event({
type: "status",
payload: { content: "Event 1" },
})

// Add second event
await handle.add.event({
type: "status",
payload: { content: "Event 2" },
})

// Add third event
await handle.add.event({
type: "status",
payload: { content: "Event 3" },
})

// Verify events are in order
const events = await adapter.workflow.events.list(handle.run.id)
expect(events.length).toBeGreaterThanOrEqual(3)

// Cleanup
const session = dataSource.getSession("WRITE")
try {
await session.run(
`
MATCH (wr:WorkflowRun {id: $id})
DETACH DELETE wr
`,
{ id: handle.run.id }
)
} finally {
await session.close()
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Event chaining test doesn’t verify ordering.

The test name says “in order” but only asserts events.length >= 3 (Line 364-365). That can pass even if ordering is wrong.

Suggested assertion
       const events = await adapter.workflow.events.list(handle.run.id)
-      expect(events.length).toBeGreaterThanOrEqual(3)
+      const statusEvents = events.filter((e) => e.type === "status")
+      expect(statusEvents.slice(-3).map((e) => (e as any).content)).toEqual([
+        "Event 1",
+        "Event 2",
+        "Event 3",
+      ])
🤖 Prompt for AI Agents
In @__tests__/shared/adapters/neo4j/workflowRuns.neo4j.test.ts around lines 338
- 379, The test currently only checks events.length but not order; update the
assertion after calling adapter.workflow.events.list(handle.run.id) to verify
sequence by inspecting the returned events array (from
adapter.workflow.events.list) for ordering—e.g., map to event payload content or
order/timestamp fields and assert that the sequence equals ["Event 1","Event
2","Event 3"] (or assert events[0].payload.content === "Event 1",
events[1].payload.content === "Event 2", events[2].payload.content === "Event
3"). Ensure you reference the returned events from
adapter.workflow.events.list(handle.run.id) and keep the cleanup block
unchanged.

Comment on lines +15 to +21
export function mapCreateWorkflowRunResult(
result: QueryResult<CreateWorkflowRunResult>,
params: CreateWorkflowRunParams
): WorkflowRun {
const record = result.records[0]
const wrNode = record.get("wr")
const wr = workflowRunSchema.parse(wrNode.properties)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing null check and unused parameter.

Two issues:

  1. params is declared but never used. Consider removing it or documenting its intended future use.
  2. result.records[0] is accessed without a null check. If the query returns no records (e.g., due to a constraint violation), this will throw an obscure error.
Proposed fix
 export function mapCreateWorkflowRunResult(
   result: QueryResult<CreateWorkflowRunResult>,
-  params: CreateWorkflowRunParams
+  _params: CreateWorkflowRunParams // Reserved for future use
 ): WorkflowRun {
   const record = result.records[0]
+  if (!record) {
+    throw new Error("Failed to create workflow run: no record returned")
+  }
   const wrNode = record.get("wr")
   const wr = workflowRunSchema.parse(wrNode.properties)
🤖 Prompt for AI Agents
In @shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.mapper.ts
around lines 15 - 21, The mapCreateWorkflowRunResult function currently declares
params but never uses it and accesses result.records[0] without checking for an
empty result; remove the unused params parameter (or document its intended
future use) and add a defensive null/length check on result.records before
accessing index 0 (e.g., ensure a record exists and throw or return a clear
error if not), then safely obtain wrNode from the validated record and call
workflowRunSchema.parse on its properties; ensure any thrown error includes
context about the createWorkflowRun query for easier debugging.

Comment on lines +15 to +23
const QUERY = `
CREATE (wr:WorkflowRun {
id: $runId,
type: $type,
postToGithub: $postToGithub,
createdAt: datetime()
})
RETURN wr
`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider idempotency for workflow runs (CREATE → MERGE by id).

If runCreate can be retried (or if a duplicate webhook delivery happens), CREATE can introduce duplicate WorkflowRun nodes unless the DB enforces uniqueness on WorkflowRun.id.

Proposed fix
 const QUERY = `
-  CREATE (wr:WorkflowRun {
-    id: $runId,
-    type: $type,
-    postToGithub: $postToGithub,
-    createdAt: datetime()
-  })
+  MERGE (wr:WorkflowRun { id: $runId })
+  ON CREATE SET
+    wr.type = $type,
+    wr.postToGithub = $postToGithub,
+    wr.createdAt = datetime()
+  ON MATCH SET
+    wr.type = $type,
+    wr.postToGithub = $postToGithub
   RETURN wr
 `

Also applies to: 46-55

🤖 Prompt for AI Agents
In @shared/src/adapters/neo4j/queries/workflowRuns/createWorkflowRun.ts around
lines 15 - 23, The CREATE of WorkflowRun is not idempotent; change the Cypher in
QUERY (and the other occurrence around the second block) to MERGE on wr by id
($runId) and use ON CREATE SET to assign properties like type, postToGithub, and
createdAt so they are set only when a new node is created, and use ON MATCH SET
if you want to update mutable fields on duplicate deliveries; update the query
constants referenced (e.g., QUERY and the similar query at lines 46-55)
accordingly.

Comment on lines +26 to +76
function mapActor(
actorNode: Node | null,
webhookEventNode: Node | null,
senderNode: Node | null,
installationId: string
): WorkflowRunActor | undefined {
// Webhook-triggered workflow
if (webhookEventNode && senderNode) {
const githubUser = githubUserSchema.parse(senderNode.properties)

// Try parsing with specific schema first, fall back to generic schema
let webhookEvent
const parseResult = githubWebhookEventSchema.safeParse(
webhookEventNode.properties
)
if (parseResult.success) {
webhookEvent = parseResult.data
} else {
// Fall back to generic schema for non-standard webhook events
webhookEvent = genericWebhookEventSchema.parse(
webhookEventNode.properties
)
}

return {
type: "webhook",
source: "github",
event: webhookEvent.event,
action: webhookEvent.action ?? "",
sender: {
id: githubUser.id,
login: githubUser.login,
},
installationId,
}
}

// User-initiated workflow
if (actorNode) {
const labels = Array.from(actorNode.labels || []) as string[]
if (labels.includes("User")) {
const user = userSchema.parse(actorNode.properties)
return {
type: "user",
userId: user.id,
}
}
}

return undefined
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prefer omitting webhook action rather than forcing empty string.

action: webhookEvent.action ?? "" (Line 54-55) can introduce meaningless empty actions. If the domain type allows optional action, pass undefined instead.

Proposed fix
-      action: webhookEvent.action ?? "",
+      ...(webhookEvent.action ? { action: webhookEvent.action } : {}),
📝 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
function mapActor(
actorNode: Node | null,
webhookEventNode: Node | null,
senderNode: Node | null,
installationId: string
): WorkflowRunActor | undefined {
// Webhook-triggered workflow
if (webhookEventNode && senderNode) {
const githubUser = githubUserSchema.parse(senderNode.properties)
// Try parsing with specific schema first, fall back to generic schema
let webhookEvent
const parseResult = githubWebhookEventSchema.safeParse(
webhookEventNode.properties
)
if (parseResult.success) {
webhookEvent = parseResult.data
} else {
// Fall back to generic schema for non-standard webhook events
webhookEvent = genericWebhookEventSchema.parse(
webhookEventNode.properties
)
}
return {
type: "webhook",
source: "github",
event: webhookEvent.event,
action: webhookEvent.action ?? "",
sender: {
id: githubUser.id,
login: githubUser.login,
},
installationId,
}
}
// User-initiated workflow
if (actorNode) {
const labels = Array.from(actorNode.labels || []) as string[]
if (labels.includes("User")) {
const user = userSchema.parse(actorNode.properties)
return {
type: "user",
userId: user.id,
}
}
}
return undefined
}
function mapActor(
actorNode: Node | null,
webhookEventNode: Node | null,
senderNode: Node | null,
installationId: string
): WorkflowRunActor | undefined {
// Webhook-triggered workflow
if (webhookEventNode && senderNode) {
const githubUser = githubUserSchema.parse(senderNode.properties)
// Try parsing with specific schema first, fall back to generic schema
let webhookEvent
const parseResult = githubWebhookEventSchema.safeParse(
webhookEventNode.properties
)
if (parseResult.success) {
webhookEvent = parseResult.data
} else {
// Fall back to generic schema for non-standard webhook events
webhookEvent = genericWebhookEventSchema.parse(
webhookEventNode.properties
)
}
return {
type: "webhook",
source: "github",
event: webhookEvent.event,
...(webhookEvent.action ? { action: webhookEvent.action } : {}),
sender: {
id: githubUser.id,
login: githubUser.login,
},
installationId,
}
}
// User-initiated workflow
if (actorNode) {
const labels = Array.from(actorNode.labels || []) as string[]
if (labels.includes("User")) {
const user = userSchema.parse(actorNode.properties)
return {
type: "user",
userId: user.id,
}
}
}
return undefined
}
🤖 Prompt for AI Agents
In @shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
around lines 26 - 76, The mapActor function should not coerce missing
webhookEvent.action to an empty string; update the webhook branch in mapActor to
set action to webhookEvent.action (which will be undefined when absent) instead
of using `?? ""`, and ensure the WorkflowRunActor type/interface accepts an
optional action so no runtime or type errors occur.

Comment on lines +95 to +143
// Validate and parse WorkflowRun
const wr = workflowRunSchema.parse(wrNode.properties)

// Parse repository to get installationId
const repo = repoNode ? repositorySchema.parse(repoNode.properties) : null
const installationId = repo?.githubInstallationId ?? ""

// Map optional relationships
const actor = mapActor(
actorNode,
webhookEventNode,
senderNode,
installationId
)
const repository = repo ? { fullName: repo.fullName } : undefined
const issue = issueNode
? (() => {
const parsed = issueSchema.parse(issueNode.properties)
return {
repoFullName: parsed.repoFullName,
number:
typeof parsed.number === "number"
? parsed.number
: parsed.number.toNumber(),
}
})()
: undefined
const commit = commitNode
? (() => {
const parsed = commitSchema.parse(commitNode.properties)
return {
sha: parsed.sha,
message: parsed.message,
repository: repository!,
}
})()
: undefined

return {
id: wr.id,
type: wr.type,
createdAt: wr.createdAt.toStandardDate(),
postToGithub: wr.postToGithub ?? false,
state: wr.state ?? "pending",
actor,
repository,
issue,
commit,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find workflowRunSchema definition and related files
fd -e ts "workflowRun" shared/src/adapters/neo4j/ | head -20

Repository: youngchingjui/issue-to-pr

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Check workflowRunSchema definition
rg -n "workflowRunSchema|WorkflowRunSchema" shared/src/adapters/neo4j/ -A 3 -B 1

Repository: youngchingjui/issue-to-pr

Length of output: 6199


🏁 Script executed:

#!/bin/bash
# Look at the getWorkflowRunById query file to see what it fetches
cat shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.ts 2>/dev/null || echo "File not found, searching for similar name"
fd "getWorkflowRunById" shared/src/adapters/neo4j/queries/

Repository: youngchingjui/issue-to-pr

Length of output: 1746


🏁 Script executed:

#!/bin/bash
# Check how listForRepo derives state from workflowState events
rg -n "listForRepo|workflowState" shared/src/adapters/neo4j/queries/workflowRuns/ -A 5 -B 2

Repository: youngchingjui/issue-to-pr

Length of output: 6520


🏁 Script executed:

#!/bin/bash
# Get complete workflowRunSchema definition
sed -n '30,50p' shared/src/adapters/neo4j/types.ts

Repository: youngchingjui/issue-to-pr

Length of output: 711


🏁 Script executed:

#!/bin/bash
# Also check if there's a workflowRunStateSchema to understand the distinction
rg -n "workflowRunStateSchema\s*=" shared/src/adapters/neo4j/types.ts -A 5

Repository: youngchingjui/issue-to-pr

Length of output: 199


State must be derived from workflowState events, not from the WorkflowRun node.

Line 138 uses state: wr.state ?? "pending", but the WorkflowRun node doesn't store state. Other query paths (listForRepo, listForIssue, listByUser) correctly derive state from the latest workflowState Event by matching (w)-[:STARTS_WITH|NEXT*]->(e:Event {type: 'workflowState'}) and using latestWorkflowState.state. The getWorkflowRunById query doesn't fetch workflowState events, so the mapper cannot correctly determine state and will always default to "pending".

Update the getWorkflowRunById query to fetch the latest workflowState event and pass it to the mapper, then use that event's state instead of the node property.

🤖 Prompt for AI Agents
In @shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
around lines 95 - 143, The mapper is incorrectly deriving state from wr.state
(workflowRun node) which is not stored there; update the getWorkflowRunById
Cypher to also MATCH the latest workflowState Event (same pattern used in
listForRepo/listForIssue/listByUser: `(w)-[:STARTS_WITH|NEXT*]->(e:Event {type:
'workflowState'})` and return it as latestWorkflowState) and pass that node into
this mapper; then change the mapping logic to use
latestWorkflowState.properties.state (or its parsed event via the event schema)
instead of wr.state (so replace `state: wr.state ?? "pending"` with the derived
`state` from latestWorkflowState, falling back to "pending" if missing). Ensure
the mapper references the returned latestWorkflowState symbol and parses it
consistently with other mappers.

Comment on lines +98 to +131
// Parse repository to get installationId
const repo = repoNode ? repositorySchema.parse(repoNode.properties) : null
const installationId = repo?.githubInstallationId ?? ""

// Map optional relationships
const actor = mapActor(
actorNode,
webhookEventNode,
senderNode,
installationId
)
const repository = repo ? { fullName: repo.fullName } : undefined
const issue = issueNode
? (() => {
const parsed = issueSchema.parse(issueNode.properties)
return {
repoFullName: parsed.repoFullName,
number:
typeof parsed.number === "number"
? parsed.number
: parsed.number.toNumber(),
}
})()
: undefined
const commit = commitNode
? (() => {
const parsed = commitSchema.parse(commitNode.properties)
return {
sha: parsed.sha,
message: parsed.message,
repository: repository!,
}
})()
: undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Crash risk: repository! can throw when commit exists but repo is null.

commit mapping sets repository: repository! (Line 128-129). If a run has a commit attached but repository isn’t attached/returned, this will throw at runtime.

Proposed fix (safe commit mapping)
-  const commit = commitNode
-    ? (() => {
-        const parsed = commitSchema.parse(commitNode.properties)
-        return {
-          sha: parsed.sha,
-          message: parsed.message,
-          repository: repository!,
-        }
-      })()
-    : undefined
+  const commit = commitNode
+    ? (() => {
+        const parsed = commitSchema.parse(commitNode.properties)
+        if (!repository) return undefined
+        return {
+          sha: parsed.sha,
+          message: parsed.message,
+          repository,
+        }
+      })()
+    : undefined

If the domain requires commit.repository always, the better fix is to update getWorkflowRunById to also fetch (commit)-[:IN_REPOSITORY]->(repo) when wr-[:BASED_ON_REPOSITORY]->(repo) is missing.

📝 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
// Parse repository to get installationId
const repo = repoNode ? repositorySchema.parse(repoNode.properties) : null
const installationId = repo?.githubInstallationId ?? ""
// Map optional relationships
const actor = mapActor(
actorNode,
webhookEventNode,
senderNode,
installationId
)
const repository = repo ? { fullName: repo.fullName } : undefined
const issue = issueNode
? (() => {
const parsed = issueSchema.parse(issueNode.properties)
return {
repoFullName: parsed.repoFullName,
number:
typeof parsed.number === "number"
? parsed.number
: parsed.number.toNumber(),
}
})()
: undefined
const commit = commitNode
? (() => {
const parsed = commitSchema.parse(commitNode.properties)
return {
sha: parsed.sha,
message: parsed.message,
repository: repository!,
}
})()
: undefined
// Parse repository to get installationId
const repo = repoNode ? repositorySchema.parse(repoNode.properties) : null
const installationId = repo?.githubInstallationId ?? ""
// Map optional relationships
const actor = mapActor(
actorNode,
webhookEventNode,
senderNode,
installationId
)
const repository = repo ? { fullName: repo.fullName } : undefined
const issue = issueNode
? (() => {
const parsed = issueSchema.parse(issueNode.properties)
return {
repoFullName: parsed.repoFullName,
number:
typeof parsed.number === "number"
? parsed.number
: parsed.number.toNumber(),
}
})()
: undefined
const commit = commitNode
? (() => {
const parsed = commitSchema.parse(commitNode.properties)
if (!repository) return undefined
return {
sha: parsed.sha,
message: parsed.message,
repository,
}
})()
: undefined
🤖 Prompt for AI Agents
In @shared/src/adapters/neo4j/queries/workflowRuns/getWorkflowRunById.mapper.ts
around lines 98 - 131, The commit mapping uses a non-null assertion repository!
which will throw if a commitNode exists but repoNode is missing; update the
commit mapping in getWorkflowRunById mapper to avoid the non-null assertion by
making commit.repository optional or deriving a repository value from the commit
properties (e.g., use parsed.repository/repoFullName from commitSchema if
present) and only set repository when you have valid data, or alternatively
ensure upstream query always returns the repo when a commit exists (fetch
(commit)-[:IN_REPOSITORY]->(repo)) if domain requires a repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI generated AI-generated Pull Requests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update workflows to create Repository/User attribution nodes

2 participants