Skip to content

ENG-13#59

Merged
Connorbelez merged 7 commits intomainfrom
ENG-13
Mar 16, 2026
Merged

ENG-13#59
Connorbelez merged 7 commits intomainfrom
ENG-13

Conversation

@Connorbelez
Copy link
Copy Markdown
Owner

@Connorbelez Connorbelez commented Mar 16, 2026

"responding to feedback"

ft. ENG-13
Implement Audit Journal Schema and Layer 2 Hash-Chaining

  • Added auditJournal table to convex/schema.ts with 16 fields and 3 indexes for tracking entity state transitions.
  • Updated AuditJournalEntry interface in convex/engine/types.ts to flatten source fields for indexability and added ENTITY_TABLE_MAP for entity type mapping.
  • Created hashChainJournalEntry mutation in convex/engine/hashChain.ts for processing journal entries with SHA-256 hash-chaining.
  • Developed reconcile query in convex/engine/reconciliation.ts to verify entity statuses against journal entries, identifying discrepancies.

These changes enhance the auditing capabilities and ensure the integrity of entity state transitions within the application.

Fix entity type check in transitionEntity function

  • Updated the entity type check from "onboardingRequests" to "onboardingRequest" for consistency.
  • Adjusted the context hydration logic to ensure compatibility with the state machine's requirements.

These changes enhance the accuracy of entity transitions and improve the overall functionality of the transitionEntity process.

Refactor error handling in hashChain processing and update onboarding queries

  • Modified the processHashChainStep function to log errors without throwing, ensuring they do not propagate to the calling workflow.
  • Updated the transitionEntity function to include a unique identifier in the journal entry ID for better traceability.
  • Changed adminMutation to adminQuery in onboarding queries for improved clarity and consistency.
  • Enhanced the role validator to dynamically include roles from REQUESTABLE_ROLES, streamlining the validation process.

These changes improve error management, enhance query clarity, and ensure better role validation in the onboarding process.

Summary by Sourcery

Introduce an audit journal schema with hash-chained audit trail integration, tighten transition engine constraints, and refine onboarding, RBAC, and ledger behavior and tests.

New Features:

  • Add an auditJournal Convex table and corresponding AuditJournalEntry type plus ENTITY_TABLE_MAP for tracking entity state transitions with rich metadata.
  • Introduce a hash-chain processing workflow and reconciliation queries to verify entity state consistency and Layer 2 audit trail integrity.
  • Add a permissions metadata sync test to ensure ROLE_PERMISSIONS stays aligned with PERMISSION_DISPLAY_METADATA.

Bug Fixes:

  • Correct the supported entity type check and machine context hydration in transitionEntity to align with the onboardingRequest state machine.
  • Fix onboarding admin queries and tests to treat request history and pending requests as queries instead of mutations.
  • Harden ledger account lookups to ignore non-POSITION accounts rather than throwing, and fix audit list React keys to use stable IDs.
  • Update onboarding member request handling to surface errors to the UI and adjust ledger input placeholders to use the correct lender ID prefix.

Enhancements:

  • Refine the onboardingRequest state machine context to be empty and expand tests to explicitly assert terminal-state immutability with clearer test naming.
  • Simplify the requestedRole validator to derive allowed roles directly from REQUESTABLE_ROLES.
  • Add reconciliation of underwriter permissions documentation and clarify test endpoint deployment expectations in ENG specs and context docs.

Documentation:

  • Add ENG-13 implementation manifest, chunk contexts, and task lists documenting the audit journal, hash-chaining, and reconciliation design and work breakdown.
  • Mark several ENG-8 and ENG-9 tasks as completed and clarify documentation snippets and comments around test-only endpoints and permission hierarchy tables.

Tests:

  • Add a new test suite to ensure role-permission mappings stay in sync with permission display metadata and validate metadata completeness.
  • Adjust onboarding request state machine and Convex onboarding tests to reflect the updated machine context and query-based admin history endpoint.

Chores:

  • Update various spec task checklists to reflect completed work and align markdown formatting for file-structure and permission examples.

Summary by CodeRabbit

  • Changes
    • Updated audit feed in RBAC authentication demo to display placeholder data

@linear
Copy link
Copy Markdown

linear bot commented Mar 16, 2026

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 16, 2026

Reviewer's Guide

Implements a host-level auditJournal table with supporting types, a durable hash-chain workflow and reconciliation queries, tightens the transition engine’s entity handling, and refines onboarding/RBAC/ledger behavior and tests for better integrity and observability.

Sequence diagram for transitionEntity to hash-chain workflow and Layer 2 auditTrail insertion

sequenceDiagram
    actor Admin
    participant Frontend
    participant TransitionMutation as transitionMutation
    participant TransitionEngine as transitionEntity
    participant AuditJournalDB as auditJournal_table
    participant HashChainModule as hashChain_module
    participant Workflow as workflow_component
    participant HashChainWorkflow as hashChainJournalEntry
    participant HashChainStep as processHashChainStep
    participant AuditTrail as auditTrail_component

    Admin->>Frontend: Trigger state transition
    Frontend->>TransitionMutation: Call engine.transitionMutation
    TransitionMutation->>TransitionEngine: transitionEntity(entityType, entityId, event)

    TransitionEngine->>AuditJournalDB: insert auditJournal(journalEntryId, entry)
    TransitionEngine->>HashChainModule: startHashChain(ctx, journalEntryId)

    HashChainModule->>Workflow: start(hashChainJournalEntry, journalEntryId)
    Workflow-->>HashChainModule: workflow_started

    loop Durable_workflow_execution
        Workflow->>HashChainWorkflow: run(journalEntryId)
        HashChainWorkflow->>HashChainStep: processHashChainStep(journalEntryId)
        HashChainStep->>AuditJournalDB: get(journalEntryId)
        alt entry_exists
            HashChainStep->>AuditTrail: insert(entityId, entityType, eventType, beforeState, afterState, metadata, timestamp)
            AuditTrail-->>HashChainStep: result_or_error
            alt error_from_auditTrail
                HashChainStep->>HashChainStep: log_error_without_throw
            else success
                HashChainStep->>HashChainStep: continue
            end
        else entry_missing
            HashChainStep->>HashChainStep: log_not_found_and_return
        end
        HashChainStep-->>HashChainWorkflow: step_complete
    end

    HashChainWorkflow-->>Workflow: workflow_complete
    TransitionEngine-->>TransitionMutation: return TransitionResult
    TransitionMutation-->>Frontend: transition_result
    Frontend-->>Admin: show_updated_status
Loading

Sequence diagram for Layer 1 and Layer 2 reconciliation queries

sequenceDiagram
    actor Operator
    participant Client as admin_client
    participant ReconcileQuery as reconcile
    participant ReconcileL2Query as reconcileLayer2
    participant AuditJournalDB as auditJournal_table
    participant Entities as governed_entities_tables
    participant AuditTrail as auditTrail_component

    Operator->>Client: Request reconciliation report
    Client->>ReconcileQuery: call reconcile()

    ReconcileQuery->>ReconcileQuery: entityTypes = keys(ENTITY_TABLE_MAP)
    loop for_each_entityType
        ReconcileQuery->>AuditJournalDB: query(by_type_and_time, entityType)
        AuditJournalDB-->>ReconcileQuery: journalEntries
        alt has_journal_entries
            ReconcileQuery->>ReconcileQuery: latestByEntity = latest_transitioned_per_entity
            loop for_each_entityId
                ReconcileQuery->>Entities: get(entityId)
                alt entity_missing
                    ReconcileQuery->>ReconcileQuery: record_discrepancy(ENTITY_NOT_FOUND)
                else entity_found
                    ReconcileQuery->>ReconcileQuery: compare(entity.status, journal.newState)
                    alt mismatch
                        ReconcileQuery->>ReconcileQuery: record_discrepancy(status_mismatch)
                    else match
                        ReconcileQuery->>ReconcileQuery: no_discrepancy
                    end
                end
            end
        else no_journal_entries
            ReconcileQuery->>ReconcileQuery: skip_entityType
        end
    end
    ReconcileQuery-->>Client: {checkedAt, discrepancies, isHealthy}

    Operator->>Client: Request Layer2 verification
    Client->>ReconcileL2Query: call reconcileLayer2()

    ReconcileL2Query->>AuditJournalDB: query(all_entries)
    AuditJournalDB-->>ReconcileL2Query: allJournalEntries
    ReconcileL2Query->>ReconcileL2Query: uniqueEntityIds = set(entityId)

    loop for_each_entityId
        ReconcileL2Query->>AuditTrail: verifyChain(entityId)
        AuditTrail-->>ReconcileL2Query: {valid, eventCount, brokenAt, error}
        alt result_present
            ReconcileL2Query->>ReconcileL2Query: push_verification(valid, eventCount, brokenAt, error)
        else no_result
            ReconcileL2Query->>ReconcileL2Query: push_verification(valid=false, error="No Layer 2 entries")
        end
    end

    ReconcileL2Query->>ReconcileL2Query: brokenChains = filter(!valid)
    ReconcileL2Query-->>Client: {checkedAt, totalEntities, verifications, brokenChains, isHealthy}
Loading

ER diagram for new auditJournal table and entity relationships

erDiagram
    onboardingRequests {
        string _id
        string status
        any machineContext
        number _creationTime
    }

    auditJournal {
        string _id
        string entityType
        string entityId
        string eventType
        any payload
        string previousState
        string newState
        string outcome
        string reason
        string actorId
        string actorType
        string channel
        string ip
        string sessionId
        string machineVersion
        string effectsScheduled
        number timestamp
    }

    onboardingRequests ||--o{ auditJournal : has_journal_entries
Loading

Class diagram for audit journaling, hash-chain workflow, and reconciliation

classDiagram
    class AuditJournalEntry {
        +string actorId
        +ActorType actorType
        +CommandChannel channel
        +string[] effectsScheduled
        +string entityId
        +EntityType entityType
        +string eventType
        +string ip
        +string machineVersion
        +string newState
        +string outcome
        +Record~string, unknown~ payload
        +string previousState
        +string reason
        +string sessionId
        +number timestamp
    }

    class ENTITY_TABLE_MAP {
        <<static>>
        +string onboardingRequest
        +string mortgage
        +string obligation
    }

    class AuditTrail {
        +AuditTrail(component)
        +Promise insert(ctx, params)
        +Promise verifyChain(ctx, params)
    }

    class WorkflowManager {
        +WorkflowManager(component)
        +Promise start(ctx, workflowDef, args)
        +WorkflowDefinition define(config)
    }

    class HashChainModule {
        +processHashChainStep(ctx, args)
        +hashChainJournalEntry(step, args)
        +startHashChain(ctx, journalEntryId)
    }

    class ReconciliationModule {
        +reconcile(ctx)
        +reconcileLayer2(ctx)
    }

    class TransitionEngine {
        +transitionEntity(ctx, params)
    }

    class AuditJournalTable {
        +index_by_entity(entityType, entityId, timestamp)
        +index_by_actor(actorId, timestamp)
        +index_by_type_and_time(entityType, timestamp)
    }

    class OnboardingRequestMachine {
        +Record~string, never~ context
        +resolveState(options)
    }

    TransitionEngine --> AuditJournalTable : writes_entries
    TransitionEngine --> HashChainModule : startHashChain

    HashChainModule --> WorkflowManager : use_for_retries
    HashChainModule --> AuditJournalTable : read_entry
    HashChainModule --> AuditTrail : insert_layer2_event

    ReconciliationModule --> AuditJournalTable : read_journal_entries
    ReconciliationModule --> AuditTrail : verifyChain

    ENTITY_TABLE_MAP <.. ReconciliationModule : maps_entityType_to_table
    ENTITY_TABLE_MAP <.. TransitionEngine : maps_entityType_to_table

    OnboardingRequestMachine <.. TransitionEngine : uses_machine_resolution
Loading

File-Level Changes

Change Details Files
Add host-level auditJournal table and align engine types for structured audit logging.
  • Introduce auditJournal Convex table with flattened source fields, outcome enums, and indexes by entity, actor, and type+time.
  • Update AuditJournalEntry interface to mirror the new schema using strong EntityType/CommandChannel/ActorType types and flattened source fields.
  • Add ENTITY_TABLE_MAP constant mapping entity types to Convex table names for future generic entity loading.
convex/schema.ts
convex/engine/types.ts
specs/ENG-13/chunks/chunk-01-schema-and-types/context.md
specs/ENG-13/chunks/chunk-01-schema-and-types/tasks.md
specs/ENG-13/tasks.md
specs/ENG-13/chunks/manifest.md
Implement Layer 2 hash-chain processing and reconciliation over the audit journal and external auditTrail component.
  • Add hashChain engine module with a workflow-managed hashChainJournalEntry process and a processHashChainStep internal mutation that logs but swallows errors.
  • Expose startHashChain helper to start the workflow from transition mutations with fire-and-forget semantics.
  • Create reconciliation query to compare latest transitioned journal state per entity against live entity.status and report discrepancies.
  • Add Layer 2 reconciliation query that walks auditTrail chains per entity and reports broken or missing chains.
  • Wire new engine/hashChain and engine/reconciliation modules into generated Convex API types and document behavior/spec context in ENG-13 chunk files.
convex/engine/hashChain.ts
convex/engine/reconciliation.ts
convex/_generated/api.d.ts
specs/ENG-13/chunks/chunk-02-hashchain-and-reconciliation/context.md
specs/ENG-13/chunks/chunk-02-hashchain-and-reconciliation/tasks.md
specs/ENG-13/tasks.md
specs/ENG-13/chunks/manifest.md
Tighten transition engine constraints and journal identifiers to be explicit and future-proof.
  • Restrict transitionEntity to the onboardingRequest entity type and throw for unsupported types.
  • Hydrate the state machine context from machineContext as a generic record instead of injecting requestId, matching the now-empty machine context.
  • Augment audit journal entry IDs with a short random UUID suffix to avoid collisions and improve traceability.
convex/engine/transition.ts
Refine onboarding flows, RBAC validation, and tests for clearer semantics and stronger guarantees.
  • Change onboarding listPendingRequests and getRequestHistory from adminMutation to adminQuery and update both UI and tests to call them as queries.
  • Simplify requestedRoleValidator to derive literals directly from REQUESTABLE_ROLES rather than manual destructuring.
  • Update onboardingRequest state machine to use empty context and expand tests to explicitly cover each terminal-state/event combination with descriptive names.
  • Refactor the onboarding admin review UI to use useQuery for pending requests and to type requestId as an Id, removing manual fetching state.
  • Improve the member request UI’s error handling by tracking and displaying requestRole errors.
  • Add a permission metadata/role-permission sync test to enforce alignment between ROLE_PERMISSIONS and PERMISSION_DISPLAY_METADATA, including known orphan handling and metadata shape checks.
convex/onboarding/queries.ts
convex/onboarding/validators.ts
convex/engine/machines/onboardingRequest.machine.ts
convex/engine/machines/__tests__/onboardingRequest.machine.test.ts
src/routes/demo/rbac-auth/onboarding.tsx
src/test/convex/onboarding/onboarding.test.ts
src/test/auth/permissions/permission-metadata-sync.test.ts
Improve ledger and audit UI robustness and clarify test/auth documentation.
  • Make ledger position account lookup robust to non-POSITION accounts by ignoring mismatches rather than throwing, and by re-checking type when falling back to non-indexed queries.
  • Update ledger UI lender ID placeholders to use demo-lender-… instead of demo-inv-… for clarity.
  • Use stable event IDs as keys in the audit event list UI instead of derived strings from action/timestamp/actor.
  • Clarify underwriter auth test endpoints to require org context and adjust ENG-8/ENG-9 spec files and task checklists to reflect completed work and deployment semantics for test endpoints.
convex/ledger/internal.ts
src/components/ledger/ledger-actions.tsx
src/routes/demo/rbac-auth/audit.tsx
convex/test/authTestEndpoints.ts
specs/ENG-8/chunks/chunk-03-queries-and-verification/tasks.md
specs/ENG-8/implementation-plan.md
specs/ENG-9/chunks/chunk-01-test-infrastructure/tasks.md
specs/ENG-9/implementation-plan.md
specs/ENG-9/chunks/chunk-03-chain-permission-tests/context.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

Warning

Rate limit exceeded

@Connorbelez has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 4 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43f681d0-5dba-4b82-9140-f21880e51ccc

📥 Commits

Reviewing files that changed from the base of the PR and between 7cf5365 and 17a83dd.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (2)
  • convex/auth/auditAuth.ts
  • convex/onboarding/validators.ts
📝 Walkthrough

Walkthrough

Configuration updates to expand test file inclusion in biome.json and tsconfig.json exclude patterns. TypeScript error suppressions added to convex files for deep generic type instantiations. Audit query component replaced with placeholder to decouple from unbuilt endpoint.

Changes

Cohort / File(s) Summary
Configuration Updates
biome.json, tsconfig.json
biome.json adds test directory to file includes; tsconfig.json adds exclude array to omit node_modules, dist, test sources, and specific paths from TypeScript compilation.
TypeScript Type Suppressions
convex/numbers.ts, convex/auth/auditAuth.ts
Added ts-expect-error directives to suppress TS2589 and handle deep generic type instantiation limits in function argument and type assertion contexts.
Component Placeholder
src/routes/demo/rbac-auth/audit.tsx
Replaced real audit events query with useQuery(undefined as any) and removed generated API import; decouples LiveAuditFeed from unbuilt audit endpoint.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 With configs set and types suppressed with care,
A rabbit hops through TypeScript's deep despair,
Test files included, endpoints tucked away,
The warren grows stronger with each passing day! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'ENG-13' is a ticket reference with no description of the actual changes made in the pull request. Replace the title with a descriptive summary of the main change, e.g., 'Add audit journal and Layer 2 hash-chaining workflow for entity state auditing'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 ENG-13
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@Connorbelez Connorbelez mentioned this pull request Mar 16, 2026
Copy link
Copy Markdown
Owner Author

Connorbelez commented Mar 16, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Connorbelez Connorbelez mentioned this pull request Mar 16, 2026
Merged
@Connorbelez Connorbelez marked this pull request as ready for review March 16, 2026 03:06
Copilot AI review requested due to automatic review settings March 16, 2026 03:06
Copy link
Copy Markdown
Owner Author

@greptile review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In reconciliation.ts, the reconcile query falls back to ctx.db.get(entityId as any) instead of using ENTITY_TABLE_MAP to resolve the concrete table type; consider wiring the map through and constructing a properly typed Id<typeof tableName> to avoid the any cast and make this more robust as new tables come online.
  • Both reconcile and reconcileLayer2 currently scan the entire auditJournal table (or all entries per type) with .collect(), which may not scale; consider adding pagination/limits or time windows so operational reconciliation doesn’t require loading all historical events into memory at once.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `reconciliation.ts`, the `reconcile` query falls back to `ctx.db.get(entityId as any)` instead of using `ENTITY_TABLE_MAP` to resolve the concrete table type; consider wiring the map through and constructing a properly typed `Id<typeof tableName>` to avoid the `any` cast and make this more robust as new tables come online.
- Both `reconcile` and `reconcileLayer2` currently scan the entire `auditJournal` table (or all entries per type) with `.collect()`, which may not scale; consider adding pagination/limits or time windows so operational reconciliation doesn’t require loading all historical events into memory at once.

## Individual Comments

### Comment 1
<location path="convex/engine/reconciliation.ts" line_range="114-121" />
<code_context>
+		const verifications: ChainVerification[] = [];
+
+		// Collect unique entityIds from the journal
+		const allEntries = await ctx.db.query("auditJournal").collect();
+		const uniqueEntityIds = new Set<string>();
+		for (const entry of allEntries) {
</code_context>
<issue_to_address>
**suggestion (performance):** Collecting all auditJournal entries in one query may not scale and could blow up memory for large histories.

This unbounded `.collect()` over `auditJournal` and in-memory de-duping of `entityIds` risks becoming very expensive or causing OOM as history grows. Consider either:

- Processing in pages (cursor / `take` pattern), or
- Leveraging an index (`by_entity` / `by_type_and_time`) to stream entities incrementally.

That way reconciliation still works when the audit log is large.

```suggestion
		const verifications: ChainVerification[] = [];

		// Collect unique entityIds from the journal in a paginated fashion to avoid
		// loading the entire audit log into memory.
		const uniqueEntityIds = new Set<string>();
		const PAGE_SIZE = 1000;
		let cursor: string | null = null;

		// Paginate through auditJournal to gather unique entityIds
		// without an unbounded collect().
		while (true) {
			const { page, isDone, continueCursor } = await ctx.db
				.query("auditJournal")
				.paginate({
					cursor,
					numItems: PAGE_SIZE,
				});

			for (const entry of page) {
				uniqueEntityIds.add(entry.entityId);
			}

			if (isDone) {
				break;
			}

			cursor = continueCursor;
		}
```
</issue_to_address>

### Comment 2
<location path="src/test/convex/onboarding/onboarding.test.ts" line_range="29" />
<code_context>
 	return t
 		.withIdentity(FAIRLEND_ADMIN)
-		.mutation(api.onboarding.queries.getRequestHistory, {
+		.query(api.onboarding.queries.getRequestHistory, {
 			requestId,
 		}) as Promise<OnboardingAuditEvent[]>;
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding tests for the new reconciliation and hash-chain behavior alongside existing onboarding audit tests

The existing onboarding audit tests are a good place to extend coverage for the new auditJournal / hash-chain / reconciliation flows. To exercise the new behavior, please add tests for:

- `reconcile` where:
  - entity status matches the latest `transitioned` journal entry,
  - entity status diverges and a discrepancy is recorded,
  - the referenced entity no longer exists (ENTITY_NOT_FOUND),
  - journal entries exist for future entity types (e.g. mortgages/obligations) without corresponding entity rows.
- `reconcileLayer2` where:
  - the hash chain is intact (valid = true),
  - the hash chain is broken/tampered (valid = false, `brokenAt` set),
  - journal entries exist but no Layer 2 entries ("missing chain").

These can live in a new `reconciliation.test.ts` or a dedicated audit/engine test file, ideally reusing the existing onboarding/audit test harness for end-to-end coverage of the new integrity guarantees.

Suggested implementation:

```typescript
	await seedFromIdentity(t, FAIRLEND_ADMIN);
	return t
		.withIdentity(FAIRLEND_ADMIN)
		.query(api.onboarding.queries.getRequestHistory, {
			requestId,
		}) as Promise<OnboardingAuditEvent[]>;
}

describe("onboarding audit reconciliation", () => {
	it("reconcile: entity status matches latest transitioned journal entry", async () => {
		const requestId = await createOnboardingRequest(t);
		const entityId = await completeOnboardingAndGetEntityId(t, requestId);

		await seedFromIdentity(t, FAIRLEND_ADMIN);
		const result = await t
			.withIdentity(FAIRLEND_ADMIN)
			.mutation(api.onboarding.mutations.reconcile, {
				entityType: "borrower",
				entityId,
			});

		expect(result.status).toBe("OK");
		expect(result.discrepancies).toEqual([]);
	});

	it("reconcile: entity status diverges and discrepancy is recorded", async () => {
		const requestId = await createOnboardingRequest(t);
		const entityId = await completeOnboardingAndGetEntityId(t, requestId);

		// Force a divergence between entity state and audit journal
		await t.withIdentity(FAIRLEND_ADMIN).mutation(api.onboarding.mutations.forceEntityStatus, {
			entityType: "borrower",
			entityId,
			status: "MANUALLY_DIVERGENT",
		});

		await seedFromIdentity(t, FAIRLEND_ADMIN);
		const result = await t
			.withIdentity(FAIRLEND_ADMIN)
			.mutation(api.onboarding.mutations.reconcile, {
				entityType: "borrower",
				entityId,
			});

		expect(result.status).toBe("DIVERGENT");
		expect(result.discrepancies.length).toBeGreaterThan(0);
		expect(result.discrepancies[0]).toHaveProperty("field");
		expect(result.discrepancies[0]).toHaveProperty("expected");
		expect(result.discrepancies[0]).toHaveProperty("actual");
	});

	it("reconcile: referenced entity no longer exists (ENTITY_NOT_FOUND)", async () => {
		const requestId = await createOnboardingRequest(t);
		const entityId = await completeOnboardingAndGetEntityId(t, requestId);

		// Delete the entity while leaving audit journal entries in place
		await t.withIdentity(FAIRLEND_ADMIN).mutation(api.onboarding.mutations.deleteEntityForReconciliationTest, {
			entityType: "borrower",
			entityId,
		});

		await seedFromIdentity(t, FAIRLEND_ADMIN);
		const result = await t
			.withIdentity(FAIRLEND_ADMIN)
			.mutation(api.onboarding.mutations.reconcile, {
				entityType: "borrower",
				entityId,
			});

		expect(result.status).toBe("ENTITY_NOT_FOUND");
	});

	it("reconcile: journal entries exist for future entity types without entity rows", async () => {
		const futureEntityId = await t
			.withIdentity(FAIRLEND_ADMIN)
			.mutation(api.onboarding.mutations.seedFutureEntityTypeJournalOnly, {
				entityType: "mortgage",
			});

		await seedFromIdentity(t, FAIRLEND_ADMIN);
		const result = await t
			.withIdentity(FAIRLEND_ADMIN)
			.mutation(api.onboarding.mutations.reconcile, {
				entityType: "mortgage",
				entityId: futureEntityId,
			});

		expect(result.status).toBe("ENTITY_NOT_FOUND");
		expect(result.hasJournal).toBe(true);
	});

	it("reconcileLayer2: hash chain is intact (valid = true)", async () => {
		const requestId = await createOnboardingRequest(t);
		await completeOnboardingAndGetEntityId(t, requestId);

		await seedFromIdentity(t, FAIRLEND_ADMIN);
		const result = await t
			.withIdentity(FAIRLEND_ADMIN)
			.query(api.onboarding.queries.reconcileLayer2, {
				scope: "onboarding",
			});

		expect(result.valid).toBe(true);
		expect(result.brokenAt).toBeNull();
		expect(result.missingChain).toBe(false);
	});

	it("reconcileLayer2: hash chain is broken/tampered (valid = false, brokenAt set)", async () => {
		const requestId = await createOnboardingRequest(t);
		await completeOnboardingAndGetEntityId(t, requestId);

		// Tamper with a single Layer 2 audit entry to break the hash chain
		await t.withIdentity(FAIRLEND_ADMIN).mutation(api.onboarding.mutations.tamperWithLayer2AuditEntry, {
			scope: "onboarding",
		});

		await seedFromIdentity(t, FAIRLEND_ADMIN);
		const result = await t
			.withIdentity(FAIRLEND_ADMIN)
			.query(api.onboarding.queries.reconcileLayer2, {
				scope: "onboarding",
			});

		expect(result.valid).toBe(false);
		expect(result.brokenAt).not.toBeNull();
		expect(result.missingChain).toBe(false);
	});

	it("reconcileLayer2: journal entries exist but no Layer 2 entries (missing chain)", async () => {
		const requestId = await createOnboardingRequest(t);
		await completeOnboardingAndGetEntityId(t, requestId);

		// Remove Layer 2 entries while leaving Layer 1 journal entries in place
		await t.withIdentity(FAIRLEND_ADMIN).mutation(api.onboarding.mutations.clearLayer2ForScope, {
			scope: "onboarding",
		});

		await seedFromIdentity(t, FAIRLEND_ADMIN);
		const result = await t
			.withIdentity(FAIRLEND_ADMIN)
			.query(api.onboarding.queries.reconcileLayer2, {
				scope: "onboarding",
			});

		expect(result.valid).toBe(false);
		expect(result.missingChain).toBe(true);
	});
});

```

To make these tests compile and pass, you will need to:

1. Align helper functions with your existing onboarding test harness:
   - Replace `createOnboardingRequest(t)` and `completeOnboardingAndGetEntityId(t, requestId)` with the actual helpers or inline flows you already use in `onboarding.test.ts` to:
     - create an onboarding request,
     - drive it through the normal workflow until the entity is persisted and audit entries are written,
     - return the `requestId` / `entityId` used by your reconciliation logic.
   - Ensure `t` (transaction/test client) and `FAIRLEND_ADMIN` are in scope for the new `describe` block, reusing the same setup pattern as the rest of the file.

2. Wire to the real Convex API names and payloads:
   - Update `api.onboarding.mutations.reconcile`, `api.onboarding.queries.reconcileLayer2` and the helper mutations (`forceEntityStatus`, `deleteEntityForReconciliationTest`, `seedFutureEntityTypeJournalOnly`, `tamperWithLayer2AuditEntry`, `clearLayer2ForScope`) to the concrete function names and argument shapes you implemented for:
     - the reconciliation engine,
     - status tampering / entity deletion test hooks,
     - hash-chain tampering / clearing Layer 2.
   - Adjust the expected result shapes:
     - For `reconcile`, use the actual fields you return for the entity-level reconciliation (e.g. `status`, `discrepancies`, `hasJournal`), and assert those instead of the placeholders if they differ.
     - For `reconcileLayer2`, adapt the assertions for `valid`, `brokenAt`, and `missingChain` to the actual fields returned by your L2 reconciliation endpoint.

3. Keep test structure consistent:
   - If you already wrap tests in a higher-level `describe("onboarding", ...)`, consider nesting this new `describe("onboarding audit reconciliation", ...)` inside it.
   - Ensure any necessary imports (e.g. `api`, `FAIRLEND_ADMIN`, helper functions) are added at the top of `onboarding.test.ts` following existing style and module paths.

These adjustments will integrate the new reconciliation and hash-chain coverage into the existing onboarding/audit tests while preserving your current testing conventions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread convex/engine/reconciliation.ts Outdated
Comment thread src/test/convex/onboarding/onboarding.test.ts
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR introduces a two-layer audit trail for entity state transitions: a Layer 1 auditJournal Convex table that records every transition with flattened source fields for indexability, and a Layer 2 SHA-256 hash-chaining workflow (hashChainJournalEntry) that asynchronously anchors each journal row into a tamper-evident audit trail component. reconcile and reconcileLayer2 admin queries verify consistency between live entity status and journal records / hash chains respectively. The PR also corrects several pre-existing issues: listPendingRequests/getRequestHistory are moved from adminMutation to adminQuery, the AdminReviewSection is switched from a manual mutation-poll to a reactive useQuery, error handling is added for admin approve/reject actions, the machine context is simplified to an empty object, and ledger account lookups are hardened against non-POSITION accounts.

Key issues found:

  • same_state_with_effects path leaks a synthetic journalEntryId — For targetless transitions that schedule effects, journalEntryId is never reassigned from its initial synthetic string ("onboardingRequest:xxx:EVENT:timestamp-uuid") before being passed to scheduleEffects and returned in TransitionResult. Effect handlers receiving this non-DB-ID value will silently get null if they attempt a ctx.db.get lookup on it.

  • computeTargetOrg lost its TypeScript exhaustiveness check — The default: { const _exhaustive: never = requestedRole; } guard was replaced with a // biome-ignore comment. Because the function return type already includes undefined, TypeScript will not catch an unhandled role added to RequestableRole, causing targetOrganizationId to silently be undefined for that role at runtime.

Confidence Score: 3/5

  • Mergeable with targeted fixes — two logic issues need resolution before production use of the audit journal.
  • The core audit journal schema, hash-chain workflow, reconciliation queries, and UI fixes are well-implemented. The prior thread issues around pagination, error re-throwing, and the listPendingRequests mutation-vs-query misclassification have all been addressed. However, two new logic issues lower confidence: the same_state_with_effects path forwards a synthetic non-DB string as journalEntryId to effect handlers and callers, which will silently break any handler that attempts a journal lookup; and the removal of the TypeScript never exhaustiveness guard in computeTargetOrg means a future role addition will silently produce targetOrganizationId: undefined at runtime without any compile-time signal.
  • convex/engine/transition.ts (synthetic journalEntryId in same_state_with_effects path) and convex/onboarding/mutations.ts (missing exhaustiveness guard in computeTargetOrg).

Important Files Changed

Filename Overview
convex/engine/transition.ts Wires appendAuditJournalEntry into the rejected and transitioned paths and fixes context hydration, but the same_state_with_effects path leaks a synthetic (non-DB) journalEntryId to effect handlers and to the returned TransitionResult.
convex/engine/hashChain.ts Implements the Layer 2 hash-chain workflow using WorkflowManager with proper error re-throwing. The startHashChain test bypass is tied to ALLOW_TEST_AUTH_ENDPOINTS, which is an auth-concern env variable, coupling two unrelated concerns; otherwise the implementation is solid.
convex/engine/reconciliation.ts Adds paginated Layer 1 and Layer 2 reconciliation queries; previous unbounded .collect() issues addressed with proper paginate() loops. getEntityStatus still returns null for mortgage/obligation types which will produce misleading ENTITY_NOT_FOUND discrepancies once those types accumulate journal entries (flagged in prior review threads).
convex/engine/auditJournal.ts New helper that inserts a Layer 1 journal entry and immediately enqueues the Layer 2 hash-chain workflow. Clean and correct; properly returns the Convex Id<"auditJournal"> to callers.
convex/onboarding/mutations.ts Wires appendAuditJournalEntry into requestRole and removes the TypeScript exhaustiveness guard from computeTargetOrg, introducing a silent undefined fallback for unhandled roles that will only surface at runtime.
convex/onboarding/queries.ts Correctly changes listPendingRequests and getRequestHistory from adminMutation to adminQuery — both handlers only read data, so this was a misclassification that is now fixed.
convex/schema.ts Adds the auditJournal table with 16 fields and 3 indexes (by_entity, by_actor, by_type_and_time). Schema matches the AuditJournalEntry interface and the source fields are correctly flattened for Convex index compatibility.
convex/engine/types.ts Updates AuditJournalEntry to flatten source fields for Convex indexability, adds ENTITY_TABLE_MAP, extends CommandChannel with onboarding_portal, and adds member to ActorType. Changes are consistent with validators and schema updates.
convex/onboarding/validators.ts Simplifies requestedRoleValidator to derive literals from REQUESTABLE_ROLES via spread. More maintainable but inherits the v.union minimum-2-args constraint flagged in prior review threads — if REQUESTABLE_ROLES ever has fewer than 2 entries, module initialization will throw.
convex/ledger/internal.ts Hardens getPositionAccount and getOrCreatePositionAccount to skip non-POSITION accounts returned by the index (instead of throwing), and removes a now-redundant type guard. The fallback full-table scan still correctly filters by type === "POSITION", so the behavior is safe.
src/routes/demo/rbac-auth/onboarding.tsx Refactors AdminReviewSection to use useQuery (reactive) instead of manually fetching with useMutation, and adds try/catch with setActionError to surface approve/reject failures to the UI — directly addressing the previously flagged unhandled-rejection issue.

Sequence Diagram

sequenceDiagram
    participant Caller as Mutation Caller
    participant TE as transitionEntity / requestRole
    participant DB as Convex DB (auditJournal)
    participant AJ as appendAuditJournalEntry
    participant HC as startHashChain
    participant WF as WorkflowManager
    participant Step as processHashChainStep
    participant AT as AuditTrail (Layer 2)
    participant RQ as reconcile / reconcileLayer2

    Caller->>TE: transitionEntity(ctx, entityType, entityId, event)
    TE->>DB: db.get(entityId) — load entity
    TE->>TE: resolveState + getNextSnapshot (pure)

    alt outcome = "rejected" (no state change, no effects)
        TE->>AJ: appendAuditJournalEntry(outcome="rejected")
        AJ->>DB: db.insert("auditJournal") → journalEntryId (real Id)
        AJ->>HC: startHashChain(ctx, journalEntryId)
        HC->>WF: workflow.start(hashChainJournalEntry)
        WF-->>HC: workflowId
    else outcome = "same_state_with_effects" (no state change, has effects)
        Note over TE: journalEntryId stays as synthetic string
        TE->>TE: scheduleEffects(journalEntryId=synthetic)
        Note over TE: ⚠ synthetic ID forwarded to effect handlers
    else outcome = "transitioned"
        TE->>DB: db.patch(entityId, newState)
        TE->>AJ: appendAuditJournalEntry(outcome="transitioned")
        AJ->>DB: db.insert("auditJournal") → journalEntryId (real Id)
        AJ->>HC: startHashChain(ctx, journalEntryId)
        HC->>WF: workflow.start(hashChainJournalEntry)
        WF-->>HC: workflowId
    end

    WF->>Step: processHashChainStep({journalEntryId})
    Step->>DB: db.get(journalEntryId)
    Step->>AT: auditTrail.insert(buildAuditTrailInsertArgs(entry))
    AT-->>Step: ok (SHA-256 hash chained)

    Caller->>RQ: reconcile({}) — Layer 1 check
    RQ->>DB: paginate auditJournal by_type_and_time
    RQ->>DB: db.get(entityId) — verify current status matches journal newState

    Caller->>RQ: reconcileLayer2({}) — Layer 2 check
    RQ->>DB: paginate auditJournal (collect unique entityIds)
    RQ->>AT: auditTrail.verifyChain(entityId)
    AT-->>RQ: {valid, eventCount, brokenAt?}
Loading

Comments Outside Diff (2)

  1. convex/engine/transition.ts, line 219-250 (link)

    Synthetic journalEntryId leaked to effect handlers and callers in same_state_with_effects path

    The journalEntryId variable is initialized as a human-readable composite string on line 161 ("onboardingRequest:xxx:EVENT:1234567890-abc12345") and is only reassigned to a real Convex Id<"auditJournal"> in the "rejected" path (line 173) and the "transitioned" path (line 259). The "same-state-with-effects" branch (lines 219–250) never calls appendAuditJournalEntry, so it passes the synthetic string in two places:

    1. scheduleEffects (line 222) — the synthetic string is forwarded to every effect handler's journalEntryId argument. If any handler tries to resolve this via ctx.db.get(args.journalEntryId as Id<"auditJournal">) it will return null (or throw a validation error) since the value is not a valid Convex document ID.

    2. Return value (line 248)TransitionResult.journalEntryId is the synthetic string, misleading any caller that stores or forwards the ID as a real journal reference.

    The fix is to either:

    • Insert a journal entry for this case too (with a new outcome value, or by extending the schema), or
    • Return journalEntryId: undefined / omit the field for this path so callers and effect handlers know no journal row exists.
    // Option A – omit journalEntryId for same_state_with_effects
    return {
        success: true,
        previousState,
        newState,
        effectsScheduled: effectNames,
    };
  2. convex/onboarding/mutations.ts, line 17-37 (link)

    Exhaustiveness guard removed — new roles silently return undefined as targetOrganizationId

    The previous default: { const _exhaustive: never = requestedRole; return _exhaustive; } block provided a TypeScript compile-time guarantee that every member of the RequestableRole union was handled. That guard has been replaced with a biome-ignore comment suppressing a linter warning.

    Without the never assignment TypeScript will not produce an error when a new role is added to REQUESTABLE_ROLES / RequestableRole. The function's return type already includes undefined, so the implicit undefined return for an unhandled case will compile cleanly and only fail at runtime — specifically it will insert an onboardingRequest document with targetOrganizationId: undefined, potentially silently producing corrupt data.

    // biome-ignore lint/style/useDefaultSwitchClause: requestedRole is an exhaustive union here.
    switch (requestedRole) {
        // ... existing cases ...
        case "sr_underwriter":
        case "admin":
            return FAIRLEND_STAFF_ORG_ID;
        default: {
            // Restore the exhaustiveness check to catch missing cases at compile time
            const _exhaustive: never = requestedRole;
            return _exhaustive;
        }
    }

    If Biome's useDefaultSwitchClause rule is what prompted this change, the // biome-ignore comment can be kept for that specific rule while restoring the never guard for TypeScript exhaustiveness.

Last reviewed commit: 807c319

Comment thread convex/engine/reconciliation.ts Outdated
Comment thread convex/engine/reconciliation.ts Outdated
Comment thread convex/engine/reconciliation.ts Outdated
Comment thread convex/engine/hashChain.ts
Comment thread convex/engine/transition.ts Outdated
Comment thread src/routes/demo/rbac-auth/onboarding.tsx
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands the governed-transition auditing infrastructure (Layer 1 auditJournal + Layer 2 auditTrail hash-chaining), adds reconciliation utilities, and refines onboarding/RBAC/test flows to align with updated query semantics and stronger integrity checks.

Changes:

  • Add auditJournal schema + updated engine types/mappings to support structured transition journaling.
  • Introduce workflow-based Layer 2 hash-chain processing and add Layer 1/Layer 2 reconciliation queries.
  • Update onboarding admin history/list endpoints to queries, tighten role validation, and add RBAC metadata sync tests; plus small demo/UI/test adjustments.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/test/convex/onboarding/onboarding.test.ts Update test helper to call getRequestHistory as a query.
src/test/auth/permissions/permission-metadata-sync.test.ts New test ensuring ROLE_PERMISSIONS and PERMISSION_DISPLAY_METADATA stay aligned.
src/routes/demo/rbac-auth/onboarding.tsx Use useQuery for pending requests, add request submission error display, tighten requestId typing.
src/routes/demo/rbac-auth/audit.tsx Use stable _id when available for audit list keys.
src/components/ledger/ledger-actions.tsx Update lender ID placeholder text.
specs/ENG-9/implementation-plan.md Documentation tweaks/clarifications around test endpoints visibility.
specs/ENG-9/chunks/chunk-03-chain-permission-tests/context.md Minor formatting adjustment.
specs/ENG-9/chunks/chunk-01-test-infrastructure/tasks.md Mark ENG-9 infra tasks complete.
specs/ENG-8/chunks/chunk-03-queries-and-verification/tasks.md Mark ENG-8 chunk tasks complete.
specs/ENG-13/tasks.md Add ENG-13 task list and completion tracking.
specs/ENG-13/chunks/manifest.md Add ENG-13 implementation manifest.
specs/ENG-13/chunks/chunk-02-hashchain-and-reconciliation/tasks.md Add chunk task list (status currently inconsistent with manifest).
specs/ENG-13/chunks/chunk-02-hashchain-and-reconciliation/context.md Add detailed context/spec for hash-chaining + reconciliation.
specs/ENG-13/chunks/chunk-01-schema-and-types/tasks.md Add chunk task list (status currently inconsistent with manifest).
specs/ENG-13/chunks/chunk-01-schema-and-types/context.md Add detailed context/spec for schema/types updates.
convex/test/authTestEndpoints.ts Update underwriter test endpoint comment to reflect org-context requirement.
convex/schema.ts Add auditJournal table and indexes.
convex/onboarding/validators.ts Derive requested-role validator literals from REQUESTABLE_ROLES.
convex/onboarding/queries.ts Convert admin onboarding endpoints from mutations to queries.
convex/ledger/internal.ts Ignore non-POSITION accounts on lookup instead of throwing.
convex/engine/types.ts Flatten AuditJournalEntry source fields; add ENTITY_TABLE_MAP.
convex/engine/transition.ts Fix entityType check; adjust machine context hydration; strengthen journalEntryId uniqueness.
convex/engine/reconciliation.ts Add Layer 1 + Layer 2 reconciliation queries.
convex/engine/hashChain.ts Add workflow-driven hash-chain processing step + starter helper.
convex/engine/machines/onboardingRequest.machine.ts Make onboarding machine context empty (Record<string, never>).
convex/engine/machines/tests/onboardingRequest.machine.test.ts Update tests for empty context and improve terminal-state coverage.
convex/_generated/api.d.ts Codegen update to include new engine modules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread convex/engine/reconciliation.ts Outdated
Comment thread convex/engine/reconciliation.ts Outdated
Comment thread convex/engine/reconciliation.ts
Comment thread convex/engine/reconciliation.ts Outdated
Comment thread convex/engine/hashChain.ts
Comment thread convex/engine/reconciliation.ts Outdated
Comment thread convex/engine/hashChain.ts
Comment thread convex/schema.ts Outdated
Comment thread specs/ENG-13/chunks/chunk-01-schema-and-types/tasks.md Outdated
Comment thread specs/ENG-13/chunks/chunk-02-hashchain-and-reconciliation/tasks.md Outdated
Copy link
Copy Markdown
Owner Author

Greptile Summary

This PR implements the ENG-13 audit journal schema and Layer 2 hash-chaining infrastructure, while also fixing a number of bugs across the transition engine, onboarding flows, ledger, and RBAC layer.

Key changes:

  • convex/schema.ts: Adds the auditJournal table (16 fields, 3 indexes) to serve as the Layer 1 structured journal of entity state transitions.
  • convex/engine/hashChain.ts (new): Defines a durable workflow (hashChainJournalEntry) and a helper (startHashChain) to propagate journal entries to the external SHA-256 auditTrail component (Layer 2). However, processHashChainStep swallows all errors, defeating the workflow's retry mechanism.
  • convex/engine/reconciliation.ts (new): Provides reconcile and reconcileLayer2 queries for integrity checking. Both queries will return false-healthy results until the transition engine is wired to write to auditJournal, and reconcileLayer2 uses an unbounded .collect() that will not scale.
  • convex/engine/transition.ts: Fixes the entity type check ("onboardingRequests""onboardingRequest") and adds a unique suffix to journalEntryId. The synthetic string format of journalEntryId is incompatible with the planned ENG-12 startHashChain wiring, which expects a real Convex Id<"auditJournal">.
  • convex/onboarding/queries.ts: Correctly changes adminMutation to adminQuery for read-only onboarding queries.
  • convex/ledger/internal.ts: Tightens POSITION account lookups to ignore non-POSITION accounts from the index.
  • convex/onboarding/validators.ts: Derives requestedRoleValidator dynamically from REQUESTABLE_ROLES, reducing duplication.
  • Tests: New permission-metadata sync test suite and expanded onboarding integration tests with thorough lifecycle coverage.

Confidence Score: 2/5

  • Needs fixes before merge — two new query/workflow files have logic issues that will produce misleading results in production.
  • The schema, types, ledger, validator, and test changes are solid. However, the two new engine files (hashChain.ts and reconciliation.ts) have connected correctness issues: (1) auditJournal is never written to by transition.ts, making all reconciliation checks return false-healthy; (2) processHashChainStep swallows errors so the wrapping workflow never retries; and (3) the synthetic journalEntryId string will be rejected by Convex's ID validator when ENG-12 wires startHashChain. These are not just future-work gaps — the reconciliation endpoints are already exposed as public queries and will mislead any consumer today.
  • convex/engine/reconciliation.ts and convex/engine/hashChain.ts require the most attention; convex/engine/transition.ts needs review to ensure the journalEntryId format aligns with the planned ENG-12 wiring.

Important Files Changed

Filename Overview
convex/engine/hashChain.ts New file implementing Layer 2 hash-chaining via a durable workflow; contains a critical design contradiction where processHashChainStep swallows all errors, making the workflow's retry mechanism ineffective.
convex/engine/reconciliation.ts New reconciliation queries for Layer 1 and Layer 2 integrity checks; both have correctness issues — reconcile returns false-healthy because auditJournal is never written to, and reconcileLayer2 uses an unbounded collect() that will fail at scale.
convex/engine/transition.ts Fixed entity type check ("onboardingRequests" → "onboardingRequest") and added a unique suffix to journalEntryId; the synthetic string ID format will be incompatible with the hashChain wiring planned for ENG-12.
convex/schema.ts Added auditJournal table with 16 fields and 3 indexes (by_entity, by_actor, by_type_and_time); schema definition looks correct and well-structured for its intended purpose.
convex/engine/types.ts Added AuditJournalEntry interface with flattened source fields and ENTITY_TABLE_MAP constant; clean and well-documented additions.
convex/onboarding/queries.ts Changed adminMutation to adminQuery for listPendingRequests and getRequestHistory; correct change since these are read-only operations.
src/routes/demo/rbac-auth/onboarding.tsx Added error handling for the member role request form; admin approve/reject handlers still lack try/catch, leaving errors as unhandled promise rejections.
src/test/convex/onboarding/onboarding.test.ts Comprehensive test suite covering the full onboarding lifecycle, re-request flows, broker_invite org resolution, and role validator rejections; well-structured with clear arrange/act/assert patterns.

Sequence Diagram

sequenceDiagram
    participant Client
    participant TransitionEngine as transition.ts
    participant DB as Convex DB
    participant AuditLog as auditLog (external)
    participant AuditJournal as auditJournal table
    participant HashChain as hashChain.ts (workflow)
    participant AuditTrail as auditTrail component (L2)

    Client->>TransitionEngine: transitionEntity(entityType, entityId, eventType)
    TransitionEngine->>DB: ctx.db.get(entityId) — load entity
    TransitionEngine->>TransitionEngine: getNextSnapshot() — compute new state
    TransitionEngine->>DB: ctx.db.patch() — persist state change
    TransitionEngine->>AuditLog: auditLog.log() — write L1 audit entry (with synthetic journalEntryId)
    TransitionEngine-->>Client: TransitionResult

    Note over AuditJournal,HashChain: ⚠️ Gap: auditJournal table is never written to by transition.ts.<br/>startHashChain() is defined but unwired (planned for ENG-12).

    Note over HashChain: When wired (ENG-12):
    TransitionEngine->>AuditJournal: ctx.db.insert("auditJournal", entry) → real Id
    TransitionEngine->>HashChain: startHashChain(ctx, journalEntryId)
    HashChain->>HashChain: workflow.define → processHashChainStep
    HashChain->>AuditJournal: ctx.db.get(journalEntryId)
    HashChain->>AuditTrail: auditTrail.insert() — SHA-256 hash-chain (L2)
Loading

Last reviewed commit: 021829c

​@greptile please re-review after latest commits

Comment thread convex/engine/reconciliation.ts Outdated
Comment on lines 4 to 6
export const requestedRoleValidator = v.union(
v.literal(brokerRole),
v.literal(lenderRole),
v.literal(lawyerRole),
v.literal(adminRole),
v.literal(jrUnderwriterRole),
v.literal(underwriterRole),
v.literal(srUnderwriterRole)
...REQUESTABLE_ROLES.map((role) => v.literal(role))
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

v.union spread will throw at module load if REQUESTABLE_ROLES has fewer than 2 entries

v.union requires a minimum of 2 arguments. If REQUESTABLE_ROLES is ever reduced to 0 or 1 elements (e.g., temporarily during refactoring or via an accidental import of a filtered constant), this line will throw at Convex module initialization time, taking down all functions in the module.

Consider adding a build-time guard or using a conditional to fall back safely:

Suggested change
export const requestedRoleValidator = v.union(
v.literal(brokerRole),
v.literal(lenderRole),
v.literal(lawyerRole),
v.literal(adminRole),
v.literal(jrUnderwriterRole),
v.literal(underwriterRole),
v.literal(srUnderwriterRole)
...REQUESTABLE_ROLES.map((role) => v.literal(role))
);
export const requestedRoleValidator =
REQUESTABLE_ROLES.length >= 2
? v.union(
...(REQUESTABLE_ROLES.map((role) => v.literal(role)) as [
ReturnType<typeof v.literal>,
ReturnType<typeof v.literal>,
...ReturnType<typeof v.literal>[],
])
)
: v.literal(REQUESTABLE_ROLES[0] ?? "");

Alternatively, add a static assertion at the top of the constants file:

import type { AssertAtLeastTwo } from "./utils";
const _: AssertAtLeastTwo<typeof REQUESTABLE_ROLES> = REQUESTABLE_ROLES;

@Connorbelez Connorbelez mentioned this pull request Mar 16, 2026
@Connorbelez Connorbelez changed the title "responding to feedback" ENG-13 Mar 16, 2026
@Connorbelez Connorbelez mentioned this pull request Mar 16, 2026
Copy link
Copy Markdown
Owner Author

@greptile please update your previous comment and give me a fresh Confidence score

@Connorbelez Connorbelez mentioned this pull request Mar 16, 2026
Implement Audit Journal Schema and Layer 2 Hash-Chaining

- Added `auditJournal` table to `convex/schema.ts` with 16 fields and 3 indexes for tracking entity state transitions.
- Updated `AuditJournalEntry` interface in `convex/engine/types.ts` to flatten source fields for indexability and added `ENTITY_TABLE_MAP` for entity type mapping.
- Created `hashChainJournalEntry` mutation in `convex/engine/hashChain.ts` for processing journal entries with SHA-256 hash-chaining.
- Developed `reconcile` query in `convex/engine/reconciliation.ts` to verify entity statuses against journal entries, identifying discrepancies.

These changes enhance the auditing capabilities and ensure the integrity of entity state transitions within the application.
…rage

- Updated various Convex functions to use fluent API methods, improving readability and consistency.
- Refactored mutations and queries to include input validation and public access where appropriate.
- Introduced new coverage configuration in `vite.config.ts` for better test reporting.
- Updated CI workflow to run unit tests with coverage tracking.
- Added new dependencies for coverage reporting and ensured proper registration of Convex components in tests.

These changes streamline function definitions, enhance test coverage, and improve overall application architecture.
@Connorbelez Connorbelez changed the base branch from 03-15-project1 to graphite-base/59 March 16, 2026 07:38
@Connorbelez Connorbelez changed the base branch from graphite-base/59 to main March 16, 2026 07:38
Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (1)
tsconfig.json (1)

3-10: Redundant node_modules exclusion patterns.

Lines 4, 7, and 8 are overlapping — "node_modules" alone is sufficient to exclude the entire directory. The other patterns ("node_modules/*", "node_modules/**/*") are redundant.

♻️ Suggested simplification
  "exclude": [
    "node_modules",
    "dist",
    "src/test/**/*",
-   "node_modules/**/*",
-   "node_modules/*",
    "node_modules/@tanstack/router-core/dist/esm/"
  ],

Note: The specific @tanstack/router-core exclusion may still be needed if you want to ensure that particular path is excluded even if node_modules handling changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tsconfig.json` around lines 3 - 10, The tsconfig.json exclude array contains
redundant entries: remove the overlapping patterns "node_modules/*" and
"node_modules/**/*" and keep the single "node_modules" entry under the "exclude"
property; retain any specific path exclusion like
"node_modules/@tanstack/router-core/dist/esm/" if you still need to explicitly
exclude that package. Ensure you update the "exclude" array to only include
"node_modules", "dist", "src/test/**/*", and the specific `@tanstack` path (if
required) to simplify and avoid duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/demo/rbac-auth/audit.tsx`:
- Around line 211-212: Replace the placeholder call to useQuery with the actual
Convex query: import { api } from "convex/_generated/api" at the top, remove the
biome-ignore/type-cast and call useQuery(api.watchCriticalAuthEvents) (i.e.,
pass the watchCriticalAuthEvents query reference from api into useQuery) so the
live audit subscription is restored; update the line that currently reads "const
events = useQuery(undefined as any)" to "const events =
useQuery(api.watchCriticalAuthEvents)".

---

Nitpick comments:
In `@tsconfig.json`:
- Around line 3-10: The tsconfig.json exclude array contains redundant entries:
remove the overlapping patterns "node_modules/*" and "node_modules/**/*" and
keep the single "node_modules" entry under the "exclude" property; retain any
specific path exclusion like "node_modules/@tanstack/router-core/dist/esm/" if
you still need to explicitly exclude that package. Ensure you update the
"exclude" array to only include "node_modules", "dist", "src/test/**/*", and the
specific `@tanstack` path (if required) to simplify and avoid duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a32c6ecd-353c-42ce-971c-5a2de9d7afa1

📥 Commits

Reviewing files that changed from the base of the PR and between e33529d and ccdc8e2.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • biome.json
  • convex/auth.ts
  • convex/demo/crons.ts
  • convex/demo/workosAuth.ts
  • convex/engine/transition.ts
  • convex/ledger/__tests__/ledger.test.ts
  • convex/numbers.ts
  • src/routes/demo/convex-triggers.tsx
  • src/routes/demo/rbac-auth/audit.tsx
  • src/routes/index.tsx
  • tsconfig.json

Comment thread src/routes/demo/rbac-auth/audit.tsx Outdated
- Removed unnecessary TypeScript error suppression comments across multiple files to improve code clarity.
- Adjusted type annotations in `transition.ts`, `auditAuth.ts`, and other files to enhance type safety and maintainability.
- Cleaned up comments related to TypeScript instantiation depth, ensuring a more consistent approach to handling deep generics.

These changes enhance the overall readability and robustness of the codebase.
@Connorbelez Connorbelez merged commit 0fe509e into main Mar 16, 2026
1 of 3 checks passed
Connorbelez added a commit that referenced this pull request Apr 20, 2026
"responding to feedback"

ft. ENG-13
Implement Audit Journal Schema and Layer 2 Hash-Chaining

- Added `auditJournal` table to `convex/schema.ts` with 16 fields and 3 indexes for tracking entity state transitions.
- Updated `AuditJournalEntry` interface in `convex/engine/types.ts` to flatten source fields for indexability and added `ENTITY_TABLE_MAP` for entity type mapping.
- Created `hashChainJournalEntry` mutation in `convex/engine/hashChain.ts` for processing journal entries with SHA-256 hash-chaining.
- Developed `reconcile` query in `convex/engine/reconciliation.ts` to verify entity statuses against journal entries, identifying discrepancies.

These changes enhance the auditing capabilities and ensure the integrity of entity state transitions within the application.

Fix entity type check in transitionEntity function

- Updated the entity type check from "onboardingRequests" to "onboardingRequest" for consistency.
- Adjusted the context hydration logic to ensure compatibility with the state machine's requirements.

These changes enhance the accuracy of entity transitions and improve the overall functionality of the transitionEntity process.

Refactor error handling in hashChain processing and update onboarding queries

- Modified the `processHashChainStep` function to log errors without throwing, ensuring they do not propagate to the calling workflow.
- Updated the `transitionEntity` function to include a unique identifier in the journal entry ID for better traceability.
- Changed `adminMutation` to `adminQuery` in onboarding queries for improved clarity and consistency.
- Enhanced the role validator to dynamically include roles from `REQUESTABLE_ROLES`, streamlining the validation process.

These changes improve error management, enhance query clarity, and ensure better role validation in the onboarding process.

## Summary by Sourcery

Introduce an audit journal schema with hash-chained audit trail integration, tighten transition engine constraints, and refine onboarding, RBAC, and ledger behavior and tests.

New Features:
- Add an auditJournal Convex table and corresponding AuditJournalEntry type plus ENTITY_TABLE_MAP for tracking entity state transitions with rich metadata.
- Introduce a hash-chain processing workflow and reconciliation queries to verify entity state consistency and Layer 2 audit trail integrity.
- Add a permissions metadata sync test to ensure ROLE_PERMISSIONS stays aligned with PERMISSION_DISPLAY_METADATA.

Bug Fixes:
- Correct the supported entity type check and machine context hydration in transitionEntity to align with the onboardingRequest state machine.
- Fix onboarding admin queries and tests to treat request history and pending requests as queries instead of mutations.
- Harden ledger account lookups to ignore non-POSITION accounts rather than throwing, and fix audit list React keys to use stable IDs.
- Update onboarding member request handling to surface errors to the UI and adjust ledger input placeholders to use the correct lender ID prefix.

Enhancements:
- Refine the onboardingRequest state machine context to be empty and expand tests to explicitly assert terminal-state immutability with clearer test naming.
- Simplify the requestedRole validator to derive allowed roles directly from REQUESTABLE_ROLES.
- Add reconciliation of underwriter permissions documentation and clarify test endpoint deployment expectations in ENG specs and context docs.

Documentation:
- Add ENG-13 implementation manifest, chunk contexts, and task lists documenting the audit journal, hash-chaining, and reconciliation design and work breakdown.
- Mark several ENG-8 and ENG-9 tasks as completed and clarify documentation snippets and comments around test-only endpoints and permission hierarchy tables.

Tests:
- Add a new test suite to ensure role-permission mappings stay in sync with permission display metadata and validate metadata completeness.
- Adjust onboarding request state machine and Convex onboarding tests to reflect the updated machine context and query-based admin history endpoint.

Chores:
- Update various spec task checklists to reflect completed work and align markdown formatting for file-structure and permission examples.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Changes**
  * Updated audit feed in RBAC authentication demo to display placeholder data
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

2 participants