From 038352857a10d93a0ba2810ac081ec904b054771 Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 07:32:50 +0700 Subject: [PATCH 01/10] feat(document): implement snapshot-sourced report rendering [PR1] --- .../markdown-impact-report.types.ts | 2 + .../traceability-section.renderer.ts | 52 ++++++--- ...eviewed-snapshot-report-context.adapter.ts | 106 ++++++++++++++++++ .../application/run-document-job.usecase.ts | 58 +--------- .../src/modules/document/document.module.ts | 3 + ...nal-reviewed-report.audit-flow.e2e-spec.ts | 54 +++++++++ docs/agent/architecture.md | 2 +- 7 files changed, 206 insertions(+), 71 deletions(-) create mode 100644 apps/api/src/modules/document/application/render/reviewed-snapshot-report-context.adapter.ts diff --git a/apps/api/src/modules/document/application/markdown-impact-report.types.ts b/apps/api/src/modules/document/application/markdown-impact-report.types.ts index 8d0f94b4..2968163c 100644 --- a/apps/api/src/modules/document/application/markdown-impact-report.types.ts +++ b/apps/api/src/modules/document/application/markdown-impact-report.types.ts @@ -41,6 +41,8 @@ export type MarkdownReportRenderContext = { dependencyEdges: ReportDependencyEdge[]; clarifications: ClarificationItemDto[]; reviewDecisions: any[]; + reviewDecisionsSnapshot?: any[]; + evidenceQualitySummarySnapshot?: any; diff?: any; metadata?: ApprovedReportMetadata; }; diff --git a/apps/api/src/modules/document/application/markdown-renderers/traceability-section.renderer.ts b/apps/api/src/modules/document/application/markdown-renderers/traceability-section.renderer.ts index 6aef44aa..ac07f318 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/traceability-section.renderer.ts +++ b/apps/api/src/modules/document/application/markdown-renderers/traceability-section.renderer.ts @@ -61,34 +61,54 @@ export function renderImpactedAreas(context: MarkdownReportRenderContext): strin } export function renderEvidenceQuality(context: MarkdownReportRenderContext): string[] { - const { traceabilityLinks } = context; + const { traceabilityLinks, reviewDecisionsSnapshot, evidenceQualitySummarySnapshot } = context; const lines: string[] = []; if (traceabilityLinks.length === 0) { return lines; } - const linkAnnotations = traceabilityLinks.map(link => ({ - link, - annotation: EvidenceQualityAnnotator.annotate(link as any) - })); - - const evidencedCount = linkAnnotations.filter(l => l.annotation.label === 'EVIDENCED' || l.annotation.label === 'WEAK_EVIDENCE').length; - const inferredCount = linkAnnotations.filter(l => l.annotation.label === 'INFERRED').length; - const reviewRequiredCount = linkAnnotations.filter(l => l.annotation.label === 'REVIEW_REQUIRED').length; - lines.push('## Evidence Quality & Dataset Readiness'); lines.push(''); - lines.push(`- Evidence-backed links: ${evidencedCount}`); - lines.push(`- Inferred links: ${inferredCount}`); - lines.push(`- Review required: ${reviewRequiredCount}`); + + if (evidenceQualitySummarySnapshot) { + const summary = evidenceQualitySummarySnapshot; + lines.push(`- Evidence-backed links: ${summary.evidenced + summary.weakEvidence}`); + lines.push(`- Inferred links: ${summary.inferred}`); + lines.push(`- Review required: ${summary.reviewRequired}`); + } else { + const linkAnnotations = traceabilityLinks.map(link => ({ + link, + annotation: EvidenceQualityAnnotator.annotate(link as any) + })); + + const evidencedCount = linkAnnotations.filter(l => l.annotation.label === 'EVIDENCED' || l.annotation.label === 'WEAK_EVIDENCE').length; + const inferredCount = linkAnnotations.filter(l => l.annotation.label === 'INFERRED').length; + const reviewRequiredCount = linkAnnotations.filter(l => l.annotation.label === 'REVIEW_REQUIRED').length; + + lines.push(`- Evidence-backed links: ${evidencedCount}`); + lines.push(`- Inferred links: ${inferredCount}`); + lines.push(`- Review required: ${reviewRequiredCount}`); + } + lines.push(''); lines.push('| Artifact | Quality | Reason |'); lines.push('|---|---|---|'); - for (const item of linkAnnotations) { - const artifactName = item.link.artifact?.filePath ? `\`${item.link.artifact.filePath}\`` : (item.link.artifact?.name || 'Unknown'); - lines.push(`| ${artifactName} | ${item.annotation.label} | ${item.annotation.reasons.join(', ')} |`); + if (reviewDecisionsSnapshot) { + for (const item of reviewDecisionsSnapshot) { + lines.push(`| \`${item.artifact}\` | ${item.quality} | ${item.reasons.join(', ')} |`); + } + } else { + const linkAnnotations = traceabilityLinks.map(link => ({ + link, + annotation: EvidenceQualityAnnotator.annotate(link as any) + })); + + for (const item of linkAnnotations) { + const artifactName = item.link.artifact?.filePath ? `\`${item.link.artifact.filePath}\`` : (item.link.artifact?.name || 'Unknown'); + lines.push(`| ${artifactName} | ${item.annotation.label} | ${item.annotation.reasons.join(', ')} |`); + } } lines.push(''); diff --git a/apps/api/src/modules/document/application/render/reviewed-snapshot-report-context.adapter.ts b/apps/api/src/modules/document/application/render/reviewed-snapshot-report-context.adapter.ts new file mode 100644 index 00000000..9ee34d01 --- /dev/null +++ b/apps/api/src/modules/document/application/render/reviewed-snapshot-report-context.adapter.ts @@ -0,0 +1,106 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../../prisma/prisma.service'; +import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { InsightRepository } from '../../../insight/infrastructure/insight.repository'; +import { TraceabilityRepository } from '../../../traceability/infrastructure/traceability.repository'; +import { ReviewNoteRepository } from '../../../impact-analysis/infrastructure/review-note.repository'; +import { GraphRepository } from '../../../graph/infrastructure/graph.repository'; +import { ClarificationRepository } from '../../../clarification/infrastructure/clarification.repository'; +import { ReviewDecisionRepository } from '../../../impact-analysis/infrastructure/review-decision.repository'; +import { GetImpactDiffUseCase } from '../../../impact-analysis/application/queries/get-impact-diff.usecase'; + +@Injectable() +export class ReviewedSnapshotReportContextAdapter { + constructor( + private readonly prisma: PrismaService, + private readonly insightRepo: InsightRepository, + private readonly traceabilityRepo: TraceabilityRepository, + private readonly reviewNoteRepo: ReviewNoteRepository, + private readonly graphRepo: GraphRepository, + private readonly clarificationRepo: ClarificationRepository, + private readonly decisionRepo: ReviewDecisionRepository, + private readonly getDiffUseCase: GetImpactDiffUseCase, + ) {} + + async buildContext(snapshot: any, analysis: any): Promise { + const analysisId = analysis.id; + + // 1. Fetch live elements + const insights = await this.insightRepo.listByAnalysis(analysisId); + let traceabilityLinks = await this.traceabilityRepo.listByAnalysis(analysisId); + let reviewNotes = await this.reviewNoteRepo.findByAnalysisId(analysisId); + const dependencyEdges = await this.graphRepo.listBySnapshot(analysis.snapshot.id); + const clarifications = await this.clarificationRepo.listByAnalysisId(analysisId); + let reviewDecisions = await this.decisionRepo.listByAnalysisId(analysisId); + + // 2. Snapshot overrides and point-in-time filtering + const snapshotDate = new Date(snapshot.createdAt); + + // Filter global review decisions and notes to only those that existed AT snapshot time + reviewDecisions = reviewDecisions.filter(d => new Date(d.createdAt) <= snapshotDate); + reviewNotes = reviewNotes.filter(n => new Date(n.createdAt) <= snapshotDate); + + // Retrieve snapshot payload + const reviewDecisionsSnapshot = snapshot.reviewDecisionsSnapshot as any[]; + const evidenceQualitySummarySnapshot = snapshot.evidenceQualitySummarySnapshot as any; + + // Overwrite traceability links with snapshot state + if (reviewDecisionsSnapshot && Array.isArray(reviewDecisionsSnapshot)) { + for (const link of traceabilityLinks) { + const snapItem = reviewDecisionsSnapshot.find(x => x.linkId === link.id); + if (snapItem) { + // Reconstruct the link's reviewDecision to match snapshot exactly + link.reviewDecision = snapItem.reviewDecision; + // Status mapping based on decision + if (snapItem.reviewDecision) { + link.reviewStatus = snapItem.reviewDecision.decision === 'ACCEPTED' ? 'CONFIRMED' : + snapItem.reviewDecision.decision === 'REJECTED' ? 'REJECTED' : 'NEEDS_REVIEW'; + } else { + link.reviewStatus = 'NEEDS_REVIEW'; + } + } + } + } + + // Determine hasUnreviewedItems from the insights (live metadata) + const hasUnreviewed = insights.some( + (insight: { reviewStatus: string }) => insight.reviewStatus === 'NEEDS_REVIEW', + ); + + let diff: any = undefined; + if (analysis.derivedFromAnalysisId) { + const diffResult = await this.getDiffUseCase.computeForAnalysis(analysisId); + if (diffResult.computable) { + diff = diffResult.diff; + } + } + + return { + analysis, + insights, + traceabilityLinks: traceabilityLinks as any[], + reviewNotes, + hasUnreviewedItems: !!hasUnreviewed, + dependencyEdges: dependencyEdges as any[], + clarifications: clarifications as any[], + reviewDecisions, + reviewDecisionsSnapshot, + evidenceQualitySummarySnapshot, + diff, + metadata: { + analysisId: analysis.id, + title: analysis.requirementRevision.title, + projectId: analysis.snapshot.repository.projectId, + repositoryId: analysis.snapshot.repositoryId, + targetRef: analysis.sourceTarget.requestedRef, + commitSha: analysis.snapshot.commitSha, + snapshotId: analysis.snapshot.id, + analyzerVersion: analysis.snapshot.analyzerVersion, + generatedDocumentId: 'pending', + generatedAt: new Date().toISOString(), + finalizedAt: analysis.updatedAt.toISOString(), + staleStatusAtReadTime: false, // Snapshot is never stale at read time + }, + }; + } +} diff --git a/apps/api/src/modules/document/application/run-document-job.usecase.ts b/apps/api/src/modules/document/application/run-document-job.usecase.ts index 851dfb19..a82e5973 100644 --- a/apps/api/src/modules/document/application/run-document-job.usecase.ts +++ b/apps/api/src/modules/document/application/run-document-job.usecase.ts @@ -10,6 +10,7 @@ import { ClarificationRepository } from '../../clarification/infrastructure/clar import { ReviewDecisionRepository } from '../../impact-analysis/infrastructure/review-decision.repository'; import { GetImpactDiffUseCase } from '../../impact-analysis/application/queries/get-impact-diff.usecase'; import { DocumentRepository } from '../infrastructure/document.repository'; +import { ReviewedSnapshotReportContextAdapter } from './render/reviewed-snapshot-report-context.adapter'; import { AppError } from '../../../shared/app-error'; @Injectable() @@ -17,14 +18,8 @@ export class RunDocumentJobUseCase { constructor( private readonly prisma: PrismaService, private readonly reportBuilder: MarkdownImpactReportBuilder, - private readonly insightRepo: InsightRepository, - private readonly traceabilityRepo: TraceabilityRepository, - private readonly reviewNoteRepo: ReviewNoteRepository, - private readonly graphRepo: GraphRepository, - private readonly clarificationRepo: ClarificationRepository, - private readonly decisionRepo: ReviewDecisionRepository, - private readonly getDiffUseCase: GetImpactDiffUseCase, private readonly documentRepo: DocumentRepository, + private readonly contextAdapter: ReviewedSnapshotReportContextAdapter, ) {} async execute(params: { documentJobId: string }) { @@ -51,7 +46,8 @@ export class RunDocumentJobUseCase { throw new AppError('IMPACT_ANALYSIS_NOT_FOUND', 'Impact analysis not found.'); } - const markdown = await this.buildMarkdown(analysis); + const context = await this.contextAdapter.buildContext(snapshot, analysis); + const markdown = this.reportBuilder.build(context); const persistedReport = await this.documentRepo.upsertApproved({ impactAnalysisId: analysis.id, content: markdown, @@ -112,53 +108,7 @@ export class RunDocumentJobUseCase { }); } - private async buildMarkdown(analysis: any) { - const analysisId = analysis.id; - const insights = await this.insightRepo.listByAnalysis(analysisId); - const traceabilityLinks = await this.traceabilityRepo.listByAnalysis(analysisId); - const reviewNotes = await this.reviewNoteRepo.findByAnalysisId(analysisId); - const dependencyEdges = await this.graphRepo.listBySnapshot(analysis.snapshot.id); - const clarifications = await this.clarificationRepo.listByAnalysisId(analysisId); - const reviewDecisions = await this.decisionRepo.listByAnalysisId(analysisId); - let diff: any = undefined; - if (analysis.derivedFromAnalysisId) { - const diffResult = await this.getDiffUseCase.computeForAnalysis(analysisId); - if (diffResult.computable) { - diff = diffResult.diff; - } - } - - const hasUnreviewed = analysis.insights?.some( - (insight: { reviewStatus: string }) => insight.reviewStatus === 'NEEDS_REVIEW', - ); - - return this.reportBuilder.build({ - analysis, - insights, - traceabilityLinks: traceabilityLinks as any[], - reviewNotes, - hasUnreviewedItems: !!hasUnreviewed, - dependencyEdges: dependencyEdges as any[], - clarifications: clarifications as any[], - reviewDecisions, - diff, - metadata: { - analysisId: analysis.id, - title: analysis.requirementRevision.title, - projectId: analysis.snapshot.repository.projectId, - repositoryId: analysis.snapshot.repositoryId, - targetRef: analysis.sourceTarget.requestedRef, - commitSha: analysis.snapshot.commitSha, - snapshotId: analysis.snapshot.id, - analyzerVersion: analysis.snapshot.analyzerVersion, - generatedDocumentId: 'pending', - generatedAt: new Date().toISOString(), - finalizedAt: analysis.updatedAt.toISOString(), - staleStatusAtReadTime: false, - }, - }); - } private toErrorJson(error: unknown) { if (error instanceof Error) { diff --git a/apps/api/src/modules/document/document.module.ts b/apps/api/src/modules/document/document.module.ts index 23f692f6..04b68beb 100644 --- a/apps/api/src/modules/document/document.module.ts +++ b/apps/api/src/modules/document/document.module.ts @@ -8,6 +8,7 @@ import { GetLatestReviewedReportSnapshotUseCase } from './application/get-latest import { GetFinalReviewedReportUseCase } from './application/get-final-reviewed-report.usecase'; import { EnqueueDocumentJobUseCase } from './application/enqueue-document-job.usecase'; import { RunDocumentJobUseCase } from './application/run-document-job.usecase'; +import { ReviewedSnapshotReportContextAdapter } from './application/render/reviewed-snapshot-report-context.adapter'; import { DocumentRepository } from './infrastructure/document.repository'; import { MarkdownImpactReportBuilder } from './application/markdown-impact-report.builder'; import { MermaidImpactDiagramBuilder } from './application/mermaid-impact-diagram.builder'; @@ -92,6 +93,7 @@ import { QueueModule } from '../queue/queue.module'; ReviewNoteRepository, ReviewDecisionRepository, GetImpactDiffUseCase, + ReviewedSnapshotReportContextAdapter, ], exports: [ DocumentRepository, @@ -103,6 +105,7 @@ import { QueueModule } from '../queue/queue.module'; GetFinalReviewedReportUseCase, EnqueueDocumentJobUseCase, RunDocumentJobUseCase, + ReviewedSnapshotReportContextAdapter, ], }) export class DocumentModule {} diff --git a/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts b/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts index ef7ea100..08d959a4 100644 --- a/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts +++ b/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts @@ -311,4 +311,58 @@ describe('Final Reviewed Report Audit Flow (e2e)', () => { expect(decisionsArray).toHaveLength(2); expect(decisionsArray.every(d => d.reviewDecision?.decision === 'ACCEPTED')).toBe(true); }); + + it('GeneratedDocument markdown reflects the snapshot payload, not the live mutated state', async () => { + const { analysisId, link1Id, link2Id } = await setupBasicAnalysisWithLinks(); + const runDocumentJob = app.get(require('../../src/modules/document/application/run-document-job.usecase').RunDocumentJobUseCase); + + // 1. Assign ACCEPTED to both links + await request(app.getHttpServer()) + .put(`/api/v1/traceability-links/${link1Id}/review-decision`) + .set('Authorization', `Bearer ${adminToken}`) + .send({ decision: 'ACCEPTED', note: 'Looks good' }) + .expect(200); + + await request(app.getHttpServer()) + .put(`/api/v1/traceability-links/${link2Id}/review-decision`) + .set('Authorization', `Bearer ${adminToken}`) + .send({ decision: 'ACCEPTED', note: 'Looks good' }) + .expect(200); + + // 2. Finalize to create Snapshot AND DocumentJob + const finalizeRes = await request(app.getHttpServer()) + .post(`/api/v1/impact-analyses/${analysisId}/finalize`) + .set('Authorization', `Bearer ${adminToken}`) + .send({ acknowledgeUnreviewed: true }) + .expect(201); + + expect(finalizeRes.body.status).toBe('COMPLETED'); + + // 3. Mutate live DB (change one decision to REJECTED) AFTER snapshot is created + // We must bypass the API because the analysis is COMPLETED and the API will reject it. + await prisma.traceabilityReviewDecision.updateMany({ + where: { traceabilityLinkId: link2Id }, + data: { decision: 'REJECTED', note: 'Actually no' }, + }); + + // 4. Run the DocumentJob (this simulates the async worker) + const documentJob = await prisma.documentJob.findFirst({ + where: { analysisId, documentType: 'IMPACT_REPORT' }, + }); + expect(documentJob).toBeDefined(); + await runDocumentJob.execute({ documentJobId: documentJob!.id }); + + // 5. Fetch the finalized document markdown + const exportRes = await request(app.getHttpServer()) + .get(`/api/v1/impact-analyses/${analysisId}/approved-report/export.md`) + .set('Authorization', `Bearer ${adminToken}`) + .expect(200); + + const markdown = exportRes.text; + + // The markdown should contain the original ACCEPTED decisions from the snapshot + expect(markdown).toContain('Confirmed'); // Corresponds to ACCEPTED in traceability section + expect(markdown).not.toContain('REJECTED'); // Should not reflect the live mutation + expect(markdown).not.toContain('Actually no'); // Should not reflect the live mutated note + }); }); diff --git a/docs/agent/architecture.md b/docs/agent/architecture.md index 703a18c1..a470b943 100644 --- a/docs/agent/architecture.md +++ b/docs/agent/architecture.md @@ -115,7 +115,7 @@ Rules: - The worker calls an application use case; it does not implement analysis. - The AI adapter returns validated structured output; it does not persist. -- Document code, and later diagram code, consumes persisted impact data. +- Document code, and later diagram code, consumes persisted impact data. Specifically, Document generation strictly derives its context from the immutable `ReviewedReportSnapshot` payload to ensure audit correctness, independent of subsequent live data mutations. - A module writes its own records; cross-module writes happen via its public application service or use case. - Controllers do not call Prisma or external SDKs directly. From b36bc510a01e90b14066e641cc7ce9903b57e96c Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 07:42:57 +0700 Subject: [PATCH 02/10] refactor(document): reorganize application folders --- .../document/api/document.controller.ts | 10 ++++---- ...create-reviewed-report-snapshot.usecase.ts | 10 ++++---- .../enqueue-document-job.usecase.ts | 6 ++--- .../{ => jobs}/run-document-job.usecase.ts | 24 +++++++++---------- .../get-final-reviewed-report.usecase.spec.ts | 2 +- .../get-final-reviewed-report.usecase.ts | 6 ++--- ...latest-reviewed-report-snapshot.usecase.ts | 4 ++-- .../{ => queries}/list-documents.usecase.ts | 2 +- ...arkdown-impact-report.builder.spec.ts.snap | 0 .../markdown-impact-report.builder.spec.ts | 4 ++-- .../markdown-impact-report.builder.ts | 6 ++--- .../evaluation-context.renderer.ts | 2 +- .../evidence-appendix.renderer.ts | 2 +- .../executive-summary.renderer.ts | 2 +- .../impact-diff.renderer.ts | 2 +- .../insight-section.renderer.ts | 2 +- .../markdown-render-utils.ts | 0 .../markdown-renderers/qa-section.renderer.ts | 2 +- .../report-header.renderer.ts | 2 +- .../review-history.renderer.ts | 2 +- .../traceability-section.renderer.ts | 4 ++-- .../finalize-impact-analysis.usecase.spec.ts | 4 ++-- .../finalize-impact-analysis.usecase.ts | 4 ++-- ...e-analysis-review-decision.usecase.spec.ts | 4 ++-- ...create-analysis-review-decision.usecase.ts | 4 ++-- ...nal-reviewed-report.audit-flow.e2e-spec.ts | 2 +- 26 files changed, 56 insertions(+), 56 deletions(-) rename apps/api/src/modules/document/application/{ => commands}/create-reviewed-report-snapshot.usecase.ts (90%) rename apps/api/src/modules/document/application/{ => commands}/enqueue-document-job.usecase.ts (95%) rename apps/api/src/modules/document/application/{ => jobs}/run-document-job.usecase.ts (75%) rename apps/api/src/modules/document/application/{ => queries}/get-final-reviewed-report.usecase.spec.ts (98%) rename apps/api/src/modules/document/application/{ => queries}/get-final-reviewed-report.usecase.ts (92%) rename apps/api/src/modules/document/application/{ => queries}/get-latest-reviewed-report-snapshot.usecase.ts (80%) rename apps/api/src/modules/document/application/{ => queries}/list-documents.usecase.ts (73%) rename apps/api/src/modules/document/application/{ => render}/__snapshots__/markdown-impact-report.builder.spec.ts.snap (100%) rename apps/api/src/modules/document/application/{ => render}/markdown-impact-report.builder.spec.ts (99%) rename apps/api/src/modules/document/application/{ => render}/markdown-impact-report.builder.ts (91%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/evaluation-context.renderer.ts (94%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/evidence-appendix.renderer.ts (94%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/executive-summary.renderer.ts (95%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/impact-diff.renderer.ts (96%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/insight-section.renderer.ts (98%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/markdown-render-utils.ts (100%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/qa-section.renderer.ts (92%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/report-header.renderer.ts (96%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/review-history.renderer.ts (89%) rename apps/api/src/modules/document/application/{ => render}/markdown-renderers/traceability-section.renderer.ts (96%) diff --git a/apps/api/src/modules/document/api/document.controller.ts b/apps/api/src/modules/document/api/document.controller.ts index ce2066bf..019fd4dd 100644 --- a/apps/api/src/modules/document/api/document.controller.ts +++ b/apps/api/src/modules/document/api/document.controller.ts @@ -1,12 +1,12 @@ import { Controller, Get, Param, Post, Body, Res } from '@nestjs/common'; import { documentListResponseSchema } from '@ba-helper/contracts'; -import { ListDocumentsUseCase } from '../application/list-documents.usecase'; +import { ListDocumentsUseCase } from '../application/queries/list-documents.usecase'; import { GetApprovedReportUseCase } from '../application/get-approved-report.usecase'; import { ExportApprovedReportUseCase } from '../application/export-approved-report.usecase'; -import { CreateReviewedReportSnapshotUseCase } from '../application/create-reviewed-report-snapshot.usecase'; -import { GetLatestReviewedReportSnapshotUseCase } from '../application/get-latest-reviewed-report-snapshot.usecase'; -import { GetFinalReviewedReportUseCase } from '../application/get-final-reviewed-report.usecase'; -import { EnqueueDocumentJobUseCase } from '../application/enqueue-document-job.usecase'; +import { CreateReviewedReportSnapshotUseCase } from '../application/commands/create-reviewed-report-snapshot.usecase'; +import { GetLatestReviewedReportSnapshotUseCase } from '../application/queries/get-latest-reviewed-report-snapshot.usecase'; +import { GetFinalReviewedReportUseCase } from '../application/queries/get-final-reviewed-report.usecase'; +import { EnqueueDocumentJobUseCase } from '../application/commands/enqueue-document-job.usecase'; import { approvedImpactReportResponseSchema, reviewedReportSnapshotSchema, diff --git a/apps/api/src/modules/document/application/create-reviewed-report-snapshot.usecase.ts b/apps/api/src/modules/document/application/commands/create-reviewed-report-snapshot.usecase.ts similarity index 90% rename from apps/api/src/modules/document/application/create-reviewed-report-snapshot.usecase.ts rename to apps/api/src/modules/document/application/commands/create-reviewed-report-snapshot.usecase.ts index 260db967..6c0dea11 100644 --- a/apps/api/src/modules/document/application/create-reviewed-report-snapshot.usecase.ts +++ b/apps/api/src/modules/document/application/commands/create-reviewed-report-snapshot.usecase.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; -import { PrismaService } from '../../prisma/prisma.service'; -import { EventLogService } from '../../event-log/application/event-log.service'; -import { TraceabilityRepository } from '../../traceability/infrastructure/traceability.repository'; -import { EvidenceQualityAnnotator } from './evidence-quality.annotator'; -import { EvaluationContextAdapter } from './evaluation-context.adapter'; +import { PrismaService } from '../../../prisma/prisma.service'; +import { EventLogService } from '../../../event-log/application/event-log.service'; +import { TraceabilityRepository } from '../../../traceability/infrastructure/traceability.repository'; +import { EvidenceQualityAnnotator } from '../evidence-quality.annotator'; +import { EvaluationContextAdapter } from '../evaluation-context.adapter'; type ReviewedReportSnapshotCreateData = { analysisId: string; diff --git a/apps/api/src/modules/document/application/enqueue-document-job.usecase.ts b/apps/api/src/modules/document/application/commands/enqueue-document-job.usecase.ts similarity index 95% rename from apps/api/src/modules/document/application/enqueue-document-job.usecase.ts rename to apps/api/src/modules/document/application/commands/enqueue-document-job.usecase.ts index eab4a514..50824b1b 100644 --- a/apps/api/src/modules/document/application/enqueue-document-job.usecase.ts +++ b/apps/api/src/modules/document/application/commands/enqueue-document-job.usecase.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; -import { PrismaService } from '../../prisma/prisma.service'; -import { QueueService } from '../../queue/queue.service'; -import { AppError } from '../../../shared/app-error'; +import { PrismaService } from '../../../prisma/prisma.service'; +import { QueueService } from '../../../queue/queue.service'; +import { AppError } from '../../../../shared/app-error'; type DocumentJobTx = Prisma.TransactionClient | PrismaService; diff --git a/apps/api/src/modules/document/application/run-document-job.usecase.ts b/apps/api/src/modules/document/application/jobs/run-document-job.usecase.ts similarity index 75% rename from apps/api/src/modules/document/application/run-document-job.usecase.ts rename to apps/api/src/modules/document/application/jobs/run-document-job.usecase.ts index a82e5973..de268fbc 100644 --- a/apps/api/src/modules/document/application/run-document-job.usecase.ts +++ b/apps/api/src/modules/document/application/jobs/run-document-job.usecase.ts @@ -1,17 +1,17 @@ import { Injectable } from '@nestjs/common'; import { DocumentJobStatus } from '@prisma/client'; -import { PrismaService } from '../../prisma/prisma.service'; -import { MarkdownImpactReportBuilder } from './markdown-impact-report.builder'; -import { InsightRepository } from '../../insight/infrastructure/insight.repository'; -import { TraceabilityRepository } from '../../traceability/infrastructure/traceability.repository'; -import { ReviewNoteRepository } from '../../impact-analysis/infrastructure/review-note.repository'; -import { GraphRepository } from '../../graph/infrastructure/graph.repository'; -import { ClarificationRepository } from '../../clarification/infrastructure/clarification.repository'; -import { ReviewDecisionRepository } from '../../impact-analysis/infrastructure/review-decision.repository'; -import { GetImpactDiffUseCase } from '../../impact-analysis/application/queries/get-impact-diff.usecase'; -import { DocumentRepository } from '../infrastructure/document.repository'; -import { ReviewedSnapshotReportContextAdapter } from './render/reviewed-snapshot-report-context.adapter'; -import { AppError } from '../../../shared/app-error'; +import { PrismaService } from '../../../prisma/prisma.service'; +import { MarkdownImpactReportBuilder } from '../render/markdown-impact-report.builder'; +import { InsightRepository } from '../../../insight/infrastructure/insight.repository'; +import { TraceabilityRepository } from '../../../traceability/infrastructure/traceability.repository'; +import { ReviewNoteRepository } from '../../../impact-analysis/infrastructure/review-note.repository'; +import { GraphRepository } from '../../../graph/infrastructure/graph.repository'; +import { ClarificationRepository } from '../../../clarification/infrastructure/clarification.repository'; +import { ReviewDecisionRepository } from '../../../impact-analysis/infrastructure/review-decision.repository'; +import { GetImpactDiffUseCase } from '../../../impact-analysis/application/queries/get-impact-diff.usecase'; +import { DocumentRepository } from '../../infrastructure/document.repository'; +import { ReviewedSnapshotReportContextAdapter } from '../render/reviewed-snapshot-report-context.adapter'; +import { AppError } from '../../../../shared/app-error'; @Injectable() export class RunDocumentJobUseCase { diff --git a/apps/api/src/modules/document/application/get-final-reviewed-report.usecase.spec.ts b/apps/api/src/modules/document/application/queries/get-final-reviewed-report.usecase.spec.ts similarity index 98% rename from apps/api/src/modules/document/application/get-final-reviewed-report.usecase.spec.ts rename to apps/api/src/modules/document/application/queries/get-final-reviewed-report.usecase.spec.ts index 4e40c6fa..9824fc33 100644 --- a/apps/api/src/modules/document/application/get-final-reviewed-report.usecase.spec.ts +++ b/apps/api/src/modules/document/application/queries/get-final-reviewed-report.usecase.spec.ts @@ -1,5 +1,5 @@ import { GetFinalReviewedReportUseCase } from './get-final-reviewed-report.usecase'; -import { AppError } from '../../../shared/app-error'; +import { AppError } from '../../../../shared/app-error'; describe('GetFinalReviewedReportUseCase', () => { let useCase: GetFinalReviewedReportUseCase; diff --git a/apps/api/src/modules/document/application/get-final-reviewed-report.usecase.ts b/apps/api/src/modules/document/application/queries/get-final-reviewed-report.usecase.ts similarity index 92% rename from apps/api/src/modules/document/application/get-final-reviewed-report.usecase.ts rename to apps/api/src/modules/document/application/queries/get-final-reviewed-report.usecase.ts index 9be3cc62..704e4dcb 100644 --- a/apps/api/src/modules/document/application/get-final-reviewed-report.usecase.ts +++ b/apps/api/src/modules/document/application/queries/get-final-reviewed-report.usecase.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { DocumentJobStatus } from '@prisma/client'; -import { AppError } from '../../../shared/app-error'; -import { GetReviewCompletionUseCase } from '../../traceability/application/get-review-completion.usecase'; +import { AppError } from '../../../../shared/app-error'; +import { GetReviewCompletionUseCase } from '../../../traceability/application/get-review-completion.usecase'; import { GetLatestReviewedReportSnapshotUseCase } from './get-latest-reviewed-report-snapshot.usecase'; import { FinalReviewedReportResponse } from '@ba-helper/contracts'; -import { PrismaService } from '../../prisma/prisma.service'; +import { PrismaService } from '../../../prisma/prisma.service'; @Injectable() export class GetFinalReviewedReportUseCase { diff --git a/apps/api/src/modules/document/application/get-latest-reviewed-report-snapshot.usecase.ts b/apps/api/src/modules/document/application/queries/get-latest-reviewed-report-snapshot.usecase.ts similarity index 80% rename from apps/api/src/modules/document/application/get-latest-reviewed-report-snapshot.usecase.ts rename to apps/api/src/modules/document/application/queries/get-latest-reviewed-report-snapshot.usecase.ts index 65380d82..b10665b7 100644 --- a/apps/api/src/modules/document/application/get-latest-reviewed-report-snapshot.usecase.ts +++ b/apps/api/src/modules/document/application/queries/get-latest-reviewed-report-snapshot.usecase.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { AppError } from '../../../shared/app-error'; +import { PrismaService } from '../../../prisma/prisma.service'; +import { AppError } from '../../../../shared/app-error'; @Injectable() export class GetLatestReviewedReportSnapshotUseCase { diff --git a/apps/api/src/modules/document/application/list-documents.usecase.ts b/apps/api/src/modules/document/application/queries/list-documents.usecase.ts similarity index 73% rename from apps/api/src/modules/document/application/list-documents.usecase.ts rename to apps/api/src/modules/document/application/queries/list-documents.usecase.ts index c73415fb..d39c6e96 100644 --- a/apps/api/src/modules/document/application/list-documents.usecase.ts +++ b/apps/api/src/modules/document/application/queries/list-documents.usecase.ts @@ -1,4 +1,4 @@ -import { DocumentRepository } from '../infrastructure/document.repository'; +import { DocumentRepository } from '../../infrastructure/document.repository'; export class ListDocumentsUseCase { constructor(private readonly repository: DocumentRepository) {} diff --git a/apps/api/src/modules/document/application/__snapshots__/markdown-impact-report.builder.spec.ts.snap b/apps/api/src/modules/document/application/render/__snapshots__/markdown-impact-report.builder.spec.ts.snap similarity index 100% rename from apps/api/src/modules/document/application/__snapshots__/markdown-impact-report.builder.spec.ts.snap rename to apps/api/src/modules/document/application/render/__snapshots__/markdown-impact-report.builder.spec.ts.snap diff --git a/apps/api/src/modules/document/application/markdown-impact-report.builder.spec.ts b/apps/api/src/modules/document/application/render/markdown-impact-report.builder.spec.ts similarity index 99% rename from apps/api/src/modules/document/application/markdown-impact-report.builder.spec.ts rename to apps/api/src/modules/document/application/render/markdown-impact-report.builder.spec.ts index 0102616a..c931e126 100644 --- a/apps/api/src/modules/document/application/markdown-impact-report.builder.spec.ts +++ b/apps/api/src/modules/document/application/render/markdown-impact-report.builder.spec.ts @@ -1,6 +1,6 @@ import { MarkdownImpactReportBuilder } from './markdown-impact-report.builder'; -import { MermaidImpactDiagramBuilder } from './mermaid-impact-diagram.builder'; -import { EvaluationContextAdapter } from './evaluation-context.adapter'; +import { MermaidImpactDiagramBuilder } from '../mermaid-impact-diagram.builder'; +import { EvaluationContextAdapter } from '../evaluation-context.adapter'; describe('MarkdownImpactReportBuilder', () => { let builder: MarkdownImpactReportBuilder; diff --git a/apps/api/src/modules/document/application/markdown-impact-report.builder.ts b/apps/api/src/modules/document/application/render/markdown-impact-report.builder.ts similarity index 91% rename from apps/api/src/modules/document/application/markdown-impact-report.builder.ts rename to apps/api/src/modules/document/application/render/markdown-impact-report.builder.ts index 3531fb12..cdd00f26 100644 --- a/apps/api/src/modules/document/application/markdown-impact-report.builder.ts +++ b/apps/api/src/modules/document/application/render/markdown-impact-report.builder.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { MermaidImpactDiagramBuilder } from './mermaid-impact-diagram.builder'; -import { EvaluationContextAdapter } from './evaluation-context.adapter'; -import { MarkdownReportRenderContext } from './markdown-impact-report.types'; +import { MermaidImpactDiagramBuilder } from '../mermaid-impact-diagram.builder'; +import { EvaluationContextAdapter } from '../evaluation-context.adapter'; +import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; import { renderReportHeader } from './markdown-renderers/report-header.renderer'; import { renderExecutiveSummary } from './markdown-renderers/executive-summary.renderer'; import { renderImpactedAreas, renderEvidenceQuality } from './markdown-renderers/traceability-section.renderer'; diff --git a/apps/api/src/modules/document/application/markdown-renderers/evaluation-context.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/evaluation-context.renderer.ts similarity index 94% rename from apps/api/src/modules/document/application/markdown-renderers/evaluation-context.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/evaluation-context.renderer.ts index 0261570f..085c0687 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/evaluation-context.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/evaluation-context.renderer.ts @@ -1,4 +1,4 @@ -import { EvaluationContextAdapter } from '../evaluation-context.adapter'; +import { EvaluationContextAdapter } from '../../evaluation-context.adapter'; export function renderEvaluationContext(evalContext: ReturnType): string[] { const lines: string[] = []; diff --git a/apps/api/src/modules/document/application/markdown-renderers/evidence-appendix.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/evidence-appendix.renderer.ts similarity index 94% rename from apps/api/src/modules/document/application/markdown-renderers/evidence-appendix.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/evidence-appendix.renderer.ts index dbfdeddf..5f22e999 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/evidence-appendix.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/evidence-appendix.renderer.ts @@ -1,4 +1,4 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; export function renderEvidenceAppendix(context: MarkdownReportRenderContext): string[] { const { insights } = context; diff --git a/apps/api/src/modules/document/application/markdown-renderers/executive-summary.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/executive-summary.renderer.ts similarity index 95% rename from apps/api/src/modules/document/application/markdown-renderers/executive-summary.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/executive-summary.renderer.ts index f1b703d6..c1e9f921 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/executive-summary.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/executive-summary.renderer.ts @@ -1,4 +1,4 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; import { resolveArtifactDisplayType } from './markdown-render-utils'; export function renderExecutiveSummary(context: MarkdownReportRenderContext, diagramResult: { mermaid: string; isTruncated: boolean }): string[] { diff --git a/apps/api/src/modules/document/application/markdown-renderers/impact-diff.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/impact-diff.renderer.ts similarity index 96% rename from apps/api/src/modules/document/application/markdown-renderers/impact-diff.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/impact-diff.renderer.ts index 8d938d17..5b90b98b 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/impact-diff.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/impact-diff.renderer.ts @@ -1,4 +1,4 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; import { formatArtifactType } from './markdown-render-utils'; export function renderImpactDiff(context: MarkdownReportRenderContext): string[] { diff --git a/apps/api/src/modules/document/application/markdown-renderers/insight-section.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/insight-section.renderer.ts similarity index 98% rename from apps/api/src/modules/document/application/markdown-renderers/insight-section.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/insight-section.renderer.ts index 595f50eb..fa5135fb 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/insight-section.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/insight-section.renderer.ts @@ -1,4 +1,4 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; import { formatCertainty } from './markdown-render-utils'; export function renderImpactsAndAc(context: MarkdownReportRenderContext): string[] { diff --git a/apps/api/src/modules/document/application/markdown-renderers/markdown-render-utils.ts b/apps/api/src/modules/document/application/render/markdown-renderers/markdown-render-utils.ts similarity index 100% rename from apps/api/src/modules/document/application/markdown-renderers/markdown-render-utils.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/markdown-render-utils.ts diff --git a/apps/api/src/modules/document/application/markdown-renderers/qa-section.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/qa-section.renderer.ts similarity index 92% rename from apps/api/src/modules/document/application/markdown-renderers/qa-section.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/qa-section.renderer.ts index e2ec5aff..42387107 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/qa-section.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/qa-section.renderer.ts @@ -1,4 +1,4 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; import { parseQaScenarioParts } from './markdown-render-utils'; export function renderQaSection(context: MarkdownReportRenderContext): string[] { diff --git a/apps/api/src/modules/document/application/markdown-renderers/report-header.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/report-header.renderer.ts similarity index 96% rename from apps/api/src/modules/document/application/markdown-renderers/report-header.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/report-header.renderer.ts index 46a6c89b..9c853903 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/report-header.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/report-header.renderer.ts @@ -1,4 +1,4 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; export function renderReportHeader(context: MarkdownReportRenderContext): string[] { const { analysis, metadata } = context; diff --git a/apps/api/src/modules/document/application/markdown-renderers/review-history.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/review-history.renderer.ts similarity index 89% rename from apps/api/src/modules/document/application/markdown-renderers/review-history.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/review-history.renderer.ts index 3739ae92..ea8cc595 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/review-history.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/review-history.renderer.ts @@ -1,4 +1,4 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; export function renderReviewHistory(context: MarkdownReportRenderContext): string[] { const { reviewDecisions } = context; diff --git a/apps/api/src/modules/document/application/markdown-renderers/traceability-section.renderer.ts b/apps/api/src/modules/document/application/render/markdown-renderers/traceability-section.renderer.ts similarity index 96% rename from apps/api/src/modules/document/application/markdown-renderers/traceability-section.renderer.ts rename to apps/api/src/modules/document/application/render/markdown-renderers/traceability-section.renderer.ts index ac07f318..19df5f07 100644 --- a/apps/api/src/modules/document/application/markdown-renderers/traceability-section.renderer.ts +++ b/apps/api/src/modules/document/application/render/markdown-renderers/traceability-section.renderer.ts @@ -1,6 +1,6 @@ -import { MarkdownReportRenderContext } from '../markdown-impact-report.types'; +import { MarkdownReportRenderContext } from '../../markdown-impact-report.types'; import { resolveArtifactDisplayType } from './markdown-render-utils'; -import { EvidenceQualityAnnotator } from '../evidence-quality.annotator'; +import { EvidenceQualityAnnotator } from '../../evidence-quality.annotator'; export function renderImpactedAreas(context: MarkdownReportRenderContext): string[] { const { analysis, traceabilityLinks, reviewNotes } = context; diff --git a/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.spec.ts b/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.spec.ts index 81570a03..4ee8354e 100644 --- a/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.spec.ts +++ b/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.spec.ts @@ -2,8 +2,8 @@ import { FinalizeImpactAnalysisUseCase } from './finalize-impact-analysis.usecas import { ImpactAnalysisRepository } from '../../infrastructure/impact-analysis.repository'; import { TraceabilityRepository } from '../../../traceability/infrastructure/traceability.repository'; import { PrismaService } from '../../../prisma/prisma.service'; -import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/create-reviewed-report-snapshot.usecase'; -import { EnqueueDocumentJobUseCase } from '../../../document/application/enqueue-document-job.usecase'; +import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/commands/create-reviewed-report-snapshot.usecase'; +import { EnqueueDocumentJobUseCase } from '../../../document/application/commands/enqueue-document-job.usecase'; describe('FinalizeImpactAnalysisUseCase', () => { let useCase: FinalizeImpactAnalysisUseCase; diff --git a/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.ts b/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.ts index 315c998f..c5f61ab3 100644 --- a/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.ts +++ b/apps/api/src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.ts @@ -7,8 +7,8 @@ import { ReviewPolicy } from '../../../review/domain/review.policy'; import { TraceabilityRepository } from '../../../traceability/infrastructure/traceability.repository'; import { PrismaService } from '../../../prisma/prisma.service'; -import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/create-reviewed-report-snapshot.usecase'; -import { EnqueueDocumentJobUseCase } from '../../../document/application/enqueue-document-job.usecase'; +import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/commands/create-reviewed-report-snapshot.usecase'; +import { EnqueueDocumentJobUseCase } from '../../../document/application/commands/enqueue-document-job.usecase'; @Injectable() export class FinalizeImpactAnalysisUseCase { diff --git a/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.spec.ts b/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.spec.ts index d3b2d860..0455d4d0 100644 --- a/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.spec.ts +++ b/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.spec.ts @@ -7,8 +7,8 @@ import { TraceabilityRepository } from '../../../traceability/infrastructure/tra import { GraphRepository } from '../../../graph/infrastructure/graph.repository'; import { ReviewNoteRepository } from '../../infrastructure/review-note.repository'; import { ClarificationRepository } from '../../../clarification/infrastructure/clarification.repository'; -import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/create-reviewed-report-snapshot.usecase'; -import { EnqueueDocumentJobUseCase } from '../../../document/application/enqueue-document-job.usecase'; +import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/commands/create-reviewed-report-snapshot.usecase'; +import { EnqueueDocumentJobUseCase } from '../../../document/application/commands/enqueue-document-job.usecase'; import { AppError } from '../../../../shared/app-error'; describe('CreateAnalysisReviewDecisionUseCase', () => { diff --git a/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.ts b/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.ts index e8aafb1c..f154468c 100644 --- a/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.ts +++ b/apps/api/src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.ts @@ -8,8 +8,8 @@ import { TraceabilityRepository } from '../../../traceability/infrastructure/tra import { GraphRepository } from '../../../graph/infrastructure/graph.repository'; import { ReviewNoteRepository } from '../../infrastructure/review-note.repository'; import { ClarificationRepository } from '../../../clarification/infrastructure/clarification.repository'; -import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/create-reviewed-report-snapshot.usecase'; -import { EnqueueDocumentJobUseCase } from '../../../document/application/enqueue-document-job.usecase'; +import { CreateReviewedReportSnapshotUseCase } from '../../../document/application/commands/create-reviewed-report-snapshot.usecase'; +import { EnqueueDocumentJobUseCase } from '../../../document/application/commands/enqueue-document-job.usecase'; import { AppError } from '../../../../shared/app-error'; import { AnalysisReviewDecisionValue } from '@prisma/client'; import { RequestUser } from '@ba-helper/contracts'; diff --git a/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts b/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts index 08d959a4..16959d0a 100644 --- a/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts +++ b/apps/api/test/e2e/final-reviewed-report.audit-flow.e2e-spec.ts @@ -314,7 +314,7 @@ describe('Final Reviewed Report Audit Flow (e2e)', () => { it('GeneratedDocument markdown reflects the snapshot payload, not the live mutated state', async () => { const { analysisId, link1Id, link2Id } = await setupBasicAnalysisWithLinks(); - const runDocumentJob = app.get(require('../../src/modules/document/application/run-document-job.usecase').RunDocumentJobUseCase); + const runDocumentJob = app.get(require('../../src/modules/document/application/jobs/run-document-job.usecase').RunDocumentJobUseCase); // 1. Assign ACCEPTED to both links await request(app.getHttpServer()) From 2bdd5275f1e537d8419abc4aa06a8e181f1c2da2 Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 07:43:00 +0700 Subject: [PATCH 03/10] refactor(document): introduce application module boundary --- .../document/document-application.module.ts | 68 +++++++++++++++++++ .../src/modules/document/document.module.ts | 52 ++------------ .../worker/document-job.worker.ts | 2 +- apps/api/test/e2e/analysis-flow.e2e-spec.ts | 2 +- .../document-job.worker.module.ts | 4 +- errors.txt | 8 +-- tests/demo/golden-path-demo.spec.ts | 2 +- .../multi-language-regression-gate.spec.ts | 2 +- 8 files changed, 85 insertions(+), 55 deletions(-) create mode 100644 apps/api/src/modules/document/document-application.module.ts diff --git a/apps/api/src/modules/document/document-application.module.ts b/apps/api/src/modules/document/document-application.module.ts new file mode 100644 index 00000000..6a95c561 --- /dev/null +++ b/apps/api/src/modules/document/document-application.module.ts @@ -0,0 +1,68 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from '../prisma/prisma.module'; +import { EventLogModule } from '../event-log/event-log.module'; +import { TraceabilityModule } from '../traceability/traceability.module'; +import { InsightModule } from '../insight/insight.module'; +import { GraphModule } from '../graph/graph.module'; +import { ClarificationModule } from '../clarification/clarification.module'; +import { QueueModule } from '../queue/queue.module'; + +import { CreateReviewedReportSnapshotUseCase } from './application/commands/create-reviewed-report-snapshot.usecase'; +import { EnqueueDocumentJobUseCase } from './application/commands/enqueue-document-job.usecase'; +import { RunDocumentJobUseCase } from './application/jobs/run-document-job.usecase'; +import { GetFinalReviewedReportUseCase } from './application/queries/get-final-reviewed-report.usecase'; +import { GetLatestReviewedReportSnapshotUseCase } from './application/queries/get-latest-reviewed-report-snapshot.usecase'; +import { ListDocumentsUseCase } from './application/queries/list-documents.usecase'; + +import { MarkdownImpactReportBuilder } from './application/render/markdown-impact-report.builder'; +import { ReviewedSnapshotReportContextAdapter } from './application/render/reviewed-snapshot-report-context.adapter'; +import { DocumentRepository } from './infrastructure/document.repository'; +import { MermaidImpactDiagramBuilder } from './application/mermaid-impact-diagram.builder'; +import { EvaluationContextAdapter } from './application/evaluation-context.adapter'; + +import { ReviewNoteRepository } from '../impact-analysis/infrastructure/review-note.repository'; +import { ReviewDecisionRepository } from '../impact-analysis/infrastructure/review-decision.repository'; +import { GetImpactDiffUseCase } from '../impact-analysis/application/queries/get-impact-diff.usecase'; + +@Module({ + imports: [ + PrismaModule, + QueueModule, + EventLogModule, + TraceabilityModule, + InsightModule, + GraphModule, + ClarificationModule, + ], + providers: [ + CreateReviewedReportSnapshotUseCase, + EnqueueDocumentJobUseCase, + RunDocumentJobUseCase, + GetFinalReviewedReportUseCase, + GetLatestReviewedReportSnapshotUseCase, + { + provide: ListDocumentsUseCase, + useFactory: (repo: DocumentRepository) => new ListDocumentsUseCase(repo), + inject: [DocumentRepository], + }, + MarkdownImpactReportBuilder, + ReviewedSnapshotReportContextAdapter, + DocumentRepository, + MermaidImpactDiagramBuilder, + EvaluationContextAdapter, + ReviewNoteRepository, + ReviewDecisionRepository, + GetImpactDiffUseCase, + ], + exports: [ + CreateReviewedReportSnapshotUseCase, + EnqueueDocumentJobUseCase, + RunDocumentJobUseCase, + GetFinalReviewedReportUseCase, + GetLatestReviewedReportSnapshotUseCase, + ListDocumentsUseCase, + DocumentRepository, + EvaluationContextAdapter, + ], +}) +export class DocumentApplicationModule {} diff --git a/apps/api/src/modules/document/document.module.ts b/apps/api/src/modules/document/document.module.ts index 04b68beb..e8353174 100644 --- a/apps/api/src/modules/document/document.module.ts +++ b/apps/api/src/modules/document/document.module.ts @@ -1,57 +1,38 @@ import { Module } from '@nestjs/common'; import { DocumentController } from './api/document.controller'; -import { ListDocumentsUseCase } from './application/list-documents.usecase'; import { GetApprovedReportUseCase } from './application/get-approved-report.usecase'; import { ExportApprovedReportUseCase } from './application/export-approved-report.usecase'; -import { CreateReviewedReportSnapshotUseCase } from './application/create-reviewed-report-snapshot.usecase'; -import { GetLatestReviewedReportSnapshotUseCase } from './application/get-latest-reviewed-report-snapshot.usecase'; -import { GetFinalReviewedReportUseCase } from './application/get-final-reviewed-report.usecase'; -import { EnqueueDocumentJobUseCase } from './application/enqueue-document-job.usecase'; -import { RunDocumentJobUseCase } from './application/run-document-job.usecase'; -import { ReviewedSnapshotReportContextAdapter } from './application/render/reviewed-snapshot-report-context.adapter'; -import { DocumentRepository } from './infrastructure/document.repository'; -import { MarkdownImpactReportBuilder } from './application/markdown-impact-report.builder'; -import { MermaidImpactDiagramBuilder } from './application/mermaid-impact-diagram.builder'; import { ApprovedReportProjectionService } from './application/approved-report-projection.service'; import { MarkdownExportRenderer } from './application/markdown-export.renderer'; import { PdfExportRenderer } from './application/pdf-export.renderer'; -import { PrismaModule } from '../prisma/prisma.module'; import { EventLogModule } from '../event-log/event-log.module'; import { EventLogService } from '../event-log/application/event-log.service'; +import { DocumentApplicationModule } from './document-application.module'; +import { DocumentRepository } from './infrastructure/document.repository'; + +import { PrismaModule } from '../prisma/prisma.module'; import { ProjectModule } from '../project/project.module'; import { TraceabilityModule } from '../traceability/traceability.module'; import { InsightModule } from '../insight/insight.module'; import { GraphModule } from '../graph/graph.module'; import { ClarificationModule } from '../clarification/clarification.module'; -import { ReviewNoteRepository } from '../impact-analysis/infrastructure/review-note.repository'; -import { ReviewDecisionRepository } from '../impact-analysis/infrastructure/review-decision.repository'; -import { GetImpactDiffUseCase } from '../impact-analysis/application/queries/get-impact-diff.usecase'; - -import { EvaluationContextAdapter } from './application/evaluation-context.adapter'; -import { QueueModule } from '../queue/queue.module'; @Module({ imports: [ - PrismaModule, + DocumentApplicationModule, EventLogModule, + PrismaModule, ProjectModule, TraceabilityModule, InsightModule, GraphModule, ClarificationModule, - QueueModule, ], controllers: [DocumentController], providers: [ - DocumentRepository, ApprovedReportProjectionService, MarkdownExportRenderer, PdfExportRenderer, - { - provide: ListDocumentsUseCase, - useFactory: (repo: DocumentRepository) => new ListDocumentsUseCase(repo), - inject: [DocumentRepository], - }, { provide: GetApprovedReportUseCase, useFactory: (repo: DocumentRepository, projection: ApprovedReportProjectionService) => @@ -82,30 +63,11 @@ import { QueueModule } from '../queue/queue.module'; PdfExportRenderer, ], }, - EvaluationContextAdapter, - MermaidImpactDiagramBuilder, - MarkdownImpactReportBuilder, - CreateReviewedReportSnapshotUseCase, - GetLatestReviewedReportSnapshotUseCase, - GetFinalReviewedReportUseCase, - EnqueueDocumentJobUseCase, - RunDocumentJobUseCase, - ReviewNoteRepository, - ReviewDecisionRepository, - GetImpactDiffUseCase, - ReviewedSnapshotReportContextAdapter, ], exports: [ - DocumentRepository, - MarkdownImpactReportBuilder, + DocumentApplicationModule, MarkdownExportRenderer, PdfExportRenderer, - CreateReviewedReportSnapshotUseCase, - GetLatestReviewedReportSnapshotUseCase, - GetFinalReviewedReportUseCase, - EnqueueDocumentJobUseCase, - RunDocumentJobUseCase, - ReviewedSnapshotReportContextAdapter, ], }) export class DocumentModule {} diff --git a/apps/api/src/modules/impact-analysis/worker/document-job.worker.ts b/apps/api/src/modules/impact-analysis/worker/document-job.worker.ts index 017f643b..a7954c09 100644 --- a/apps/api/src/modules/impact-analysis/worker/document-job.worker.ts +++ b/apps/api/src/modules/impact-analysis/worker/document-job.worker.ts @@ -1,6 +1,6 @@ import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Job } from 'bullmq'; -import { RunDocumentJobUseCase } from '../../document/application/run-document-job.usecase'; +import { RunDocumentJobUseCase } from '../../document/application/jobs/run-document-job.usecase'; @Processor('document-job') export class DocumentJobWorker extends WorkerHost { diff --git a/apps/api/test/e2e/analysis-flow.e2e-spec.ts b/apps/api/test/e2e/analysis-flow.e2e-spec.ts index e8f454a4..ac5faf97 100644 --- a/apps/api/test/e2e/analysis-flow.e2e-spec.ts +++ b/apps/api/test/e2e/analysis-flow.e2e-spec.ts @@ -18,7 +18,7 @@ import { impactAnalysisResponseSchema, approvedImpactReportResponseSchema, } from '@ba-helper/contracts'; -import { RunDocumentJobUseCase } from '../../src/modules/document/application/run-document-job.usecase'; +import { RunDocumentJobUseCase } from '../../src/modules/document/application/jobs/run-document-job.usecase'; describe('Analysis Flow (E2E)', () => { let app: INestApplication; diff --git a/apps/worker/src/document-job/document-job.worker.module.ts b/apps/worker/src/document-job/document-job.worker.module.ts index d0e15035..a9803c2f 100644 --- a/apps/worker/src/document-job/document-job.worker.module.ts +++ b/apps/worker/src/document-job/document-job.worker.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; -import { DocumentModule } from '../../../api/src/modules/document/document.module'; +import { DocumentApplicationModule } from '../../../api/src/modules/document/document-application.module'; import { DocumentJobWorker } from '../../../api/src/modules/impact-analysis/worker/document-job.worker'; @Module({ - imports: [DocumentModule], + imports: [DocumentApplicationModule], providers: [DocumentJobWorker], }) export class DocumentJobWorkerModule {} diff --git a/errors.txt b/errors.txt index 9db0afc0..8636e070 100644 --- a/errors.txt +++ b/errors.txt @@ -1,9 +1,9 @@ src/modules/document/api/document.controller.ts(1,46): error TS2300: Duplicate identifier 'Res'. src/modules/document/api/document.controller.ts(18,10): error TS2300: Duplicate identifier 'Res'. -src/modules/document/application/create-reviewed-report-snapshot.usecase.ts(63,11): error TS2322: Type 'null' is not assignable to type 'string'. -src/modules/document/application/create-reviewed-report-snapshot.usecase.ts(66,11): error TS2322: Type 'EvaluationContext | null' is not assignable to type 'InputJsonValue | NullableJsonNullValueInput | undefined'. +src/modules/document/application/commands/create-reviewed-report-snapshot.usecase.ts(63,11): error TS2322: Type 'null' is not assignable to type 'string'. +src/modules/document/application/commands/create-reviewed-report-snapshot.usecase.ts(66,11): error TS2322: Type 'EvaluationContext | null' is not assignable to type 'InputJsonValue | NullableJsonNullValueInput | undefined'. Type 'null' is not assignable to type 'InputJsonValue | NullableJsonNullValueInput | undefined'. -src/modules/document/application/enqueue-document-job.usecase.ts(94,13): error TS2322: Type 'null' is not assignable to type 'InputJsonValue | NullableJsonNullValueInput | undefined'. +src/modules/document/application/commands/enqueue-document-job.usecase.ts(94,13): error TS2322: Type 'null' is not assignable to type 'InputJsonValue | NullableJsonNullValueInput | undefined'. src/modules/document/document.module.ts(24,35): error TS2307: Cannot find module './worker/document-job.worker' or its corresponding type declarations. src/modules/impact-analysis/api/impact-analysis.controller.ts(413,58): error TS2345: Argument of type '{ analysisId: string; acknowledgeUnreviewed: boolean; }' is not assignable to parameter of type '{ analysisId: string; acknowledgeUnreviewed: boolean; userId: string; }'. Property 'userId' is missing in type '{ analysisId: string; acknowledgeUnreviewed: boolean; }' but required in type '{ analysisId: string; acknowledgeUnreviewed: boolean; userId: string; }'. @@ -17,7 +17,7 @@ src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.useca src/modules/impact-analysis/application/lifecycle/finalize-impact-analysis.usecase.spec.ts(257,27): error TS2345: Argument of type '{ analysisId: string; acknowledgeUnreviewed: true; }' is not assignable to parameter of type '{ analysisId: string; acknowledgeUnreviewed: boolean; userId: string; }'. Property 'userId' is missing in type '{ analysisId: string; acknowledgeUnreviewed: true; }' but required in type '{ analysisId: string; acknowledgeUnreviewed: boolean; userId: string; }'. src/modules/impact-analysis/application/review/create-analysis-review-decision.usecase.spec.ts(74,15): error TS2554: Expected 11 arguments, but got 10. -src/modules/impact-analysis/worker/document-job.worker.ts(4,45): error TS2307: Cannot find module '../application/markdown-impact-report.builder' or its corresponding type declarations. +src/modules/impact-analysis/worker/document-job.worker.ts(4,45): error TS2307: Cannot find module '../application/render/markdown-impact-report.builder' or its corresponding type declarations. src/modules/impact-analysis/worker/document-job.worker.ts(12,36): error TS2307: Cannot find module '../infrastructure/document.repository' or its corresponding type declarations. /home/diphungthinh/Desktop/BA_helper_test/apps/api: [ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL] @ba-helper/api@0.1.0 typecheck: `tsc --noEmit` diff --git a/tests/demo/golden-path-demo.spec.ts b/tests/demo/golden-path-demo.spec.ts index 0d7a5c3f..8eb4303f 100644 --- a/tests/demo/golden-path-demo.spec.ts +++ b/tests/demo/golden-path-demo.spec.ts @@ -9,7 +9,7 @@ import { FinalizeImpactAnalysisUseCase } from '../../apps/api/src/modules/impact import { GetRepositorySnapshotDriftUseCase } from '../../apps/api/src/modules/repository/application/get-repository-snapshot-drift.usecase'; import { ScanJobStatus } from '@prisma/client'; import { resolve } from 'node:path'; -import { RunDocumentJobUseCase } from '../../apps/api/src/modules/document/application/run-document-job.usecase'; +import { RunDocumentJobUseCase } from '../../apps/api/src/modules/document/application/jobs/run-document-job.usecase'; describe('Golden Path Demo', () => { let app: any; diff --git a/tests/impact-analysis/multi-language-regression-gate.spec.ts b/tests/impact-analysis/multi-language-regression-gate.spec.ts index d5bd1e3f..72b7d749 100644 --- a/tests/impact-analysis/multi-language-regression-gate.spec.ts +++ b/tests/impact-analysis/multi-language-regression-gate.spec.ts @@ -11,7 +11,7 @@ import { ScanJobStatus } from '@prisma/client'; import { resolve, join } from 'node:path'; import * as fs from 'node:fs/promises'; import * as os from 'node:os'; -import { RunDocumentJobUseCase } from '../../apps/api/src/modules/document/application/run-document-job.usecase'; +import { RunDocumentJobUseCase } from '../../apps/api/src/modules/document/application/jobs/run-document-job.usecase'; const safeRm = async (targetPath: string) => { await fs.rm(targetPath, { recursive: true, force: true }).catch(() => {}); From 085a903690972bc389ff70f421059ec7d6788334 Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 07:51:52 +0700 Subject: [PATCH 04/10] refactor(api): split impact analysis controllers --- apps/api/package.json | 1 + .../impact-analysis-lifecycle.controller.ts | 176 ++++++++++++ ...ct-analysis-read-model.controller.spec.ts} | 28 +- .../impact-analysis-read-model.controller.ts | 156 +++++++++++ .../api/impact-analysis-review.controller.ts | 141 ++++++++++ ...r.ts => multi-repo-analysis.controller.ts} | 250 +----------------- .../impact-analysis/impact-analysis.module.ts | 14 +- pnpm-lock.yaml | 13 +- 8 files changed, 499 insertions(+), 280 deletions(-) create mode 100644 apps/api/src/modules/impact-analysis/api/impact-analysis-lifecycle.controller.ts rename apps/api/src/modules/impact-analysis/api/{impact-analysis.controller.spec.ts => impact-analysis-read-model.controller.spec.ts} (65%) create mode 100644 apps/api/src/modules/impact-analysis/api/impact-analysis-read-model.controller.ts create mode 100644 apps/api/src/modules/impact-analysis/api/impact-analysis-review.controller.ts rename apps/api/src/modules/impact-analysis/api/{impact-analysis.controller.ts => multi-repo-analysis.controller.ts} (55%) diff --git a/apps/api/package.json b/apps/api/package.json index e96bd1b1..dd2efe6b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -54,6 +54,7 @@ "jest": "30.4.2", "prisma": "7.8.0", "ts-jest": "^29.4.11", + "ts-morph": "28.0.0", "tsx": "^4.22.3" } } diff --git a/apps/api/src/modules/impact-analysis/api/impact-analysis-lifecycle.controller.ts b/apps/api/src/modules/impact-analysis/api/impact-analysis-lifecycle.controller.ts new file mode 100644 index 00000000..3727f67d --- /dev/null +++ b/apps/api/src/modules/impact-analysis/api/impact-analysis-lifecycle.controller.ts @@ -0,0 +1,176 @@ +import { Body, Controller, Get, Param, Post, Query, BadRequestException, NotFoundException, Res } from '@nestjs/common'; +import { + impactAnalysisCreateRequestSchema, + impactAnalysisListResponseSchema, + impactAnalysisResponseSchema, + multiRepoImpactAnalysisCreateRequestSchema, + multiRepoImpactAnalysisCreateResponseSchema, + multiRepoAnalysisRunDetailResponseSchema, + multiRepoAnalysisRunListResponseSchema, + multiRepoImpactMatrixResponseSchema, + multiRepoMergedReportDraftResponseSchema, + multiRepoApprovedReportResponseSchema, + mergedMultiRepoReportReviewDecisionCreateResponseSchema, + mergedMultiRepoReportReviewDecisionListResponseSchema, + mergedMultiRepoReportReviewDecisionResponseSchema, + finalizeImpactAnalysisRequestSchema, + impactGraphResponseSchema, + qaCoverageResponseSchema, + reviewQueueResponseSchema, + paginationQuerySchema, + impactAnalysisDiffResponseSchema, + reviewDecisionRequestSchema, + reviewDecisionCreateResponseSchema, + reviewDecisionListResponseSchema, + reviewDecisionResponseSchema, + lineageTimelineResponseSchema, + driftFreshnessRecommendationSchema, + RequestUser, +} from '@ba-helper/contracts'; +import { CurrentUser } from '../../auth/api/current-user.decorator'; +import { CreateImpactAnalysisUseCase } from '../application/lifecycle/create-impact-analysis.usecase'; +import { CreateMultiRepoImpactAnalysesUseCase } from '../application/multi-repo/create-multi-repo-impact-analyses.usecase'; +import { GetImpactAnalysisUseCase } from '../application/lifecycle/get-impact-analysis.usecase'; +import { GetMultiRepoAnalysisRunUseCase } from '../application/multi-repo/get-multi-repo-analysis-run.usecase'; +import { BuildMultiRepoImpactMatrixReadModel } from '../application/multi-repo/build-multi-repo-impact-matrix.read-model'; +import { GetMatrixRowDetailUseCase } from '../application/queries/get-matrix-row-detail.usecase'; +import { GetMergedMultiRepoReportDraftUseCase } from '../application/multi-repo/get-merged-multi-repo-report-draft.usecase'; +import { FinalizeMultiRepoReportUseCase } from '../application/multi-repo/finalize-multi-repo-report.usecase'; +import { GetApprovedMultiRepoReportUseCase } from '../application/multi-repo/get-approved-multi-repo-report.usecase'; +import { ExportApprovedMultiRepoReportUseCase } from '../application/multi-repo/export-approved-multi-repo-report.usecase'; +import { ListMultiRepoAnalysisRunsUseCase } from '../application/multi-repo/list-multi-repo-analysis-runs.usecase'; +import { CreateMergedMultiRepoReportReviewDecisionUseCase } from '../application/multi-repo/create-merged-multi-repo-report-review-decision.usecase'; +import { ListMergedMultiRepoReportReviewDecisionsUseCase } from '../application/multi-repo/list-merged-multi-repo-report-review-decisions.usecase'; +import { GetLatestMergedMultiRepoReportReviewDecisionUseCase } from '../application/multi-repo/get-latest-merged-multi-repo-report-review-decision.usecase'; +import { FinalizeImpactAnalysisUseCase } from '../application/lifecycle/finalize-impact-analysis.usecase'; +import { ListImpactAnalysesUseCase } from '../application/lifecycle/list-impact-analyses.usecase'; +import { GetImpactGraphUseCase } from '../application/queries/get-impact-graph.usecase'; +import { GetQaCoverageUseCase } from '../application/qa/get-qa-coverage.usecase'; +import { GetReviewQueueUseCase } from '../application/review/get-review-queue.usecase'; +import { GetImpactDiffUseCase } from '../application/queries/get-impact-diff.usecase'; +import { CreateAnalysisReviewDecisionUseCase } from '../application/review/create-analysis-review-decision.usecase'; +import { ListReviewDecisionsUseCase } from '../application/review/list-review-decisions.usecase'; +import { GetLatestReviewDecisionUseCase } from '../application/review/get-latest-review-decision.usecase'; +import { GetImpactAnalysisLineageUseCase } from '../application/queries/get-impact-analysis-lineage.usecase'; +import { GetReviewCoverageUseCase } from '../application/review/get-review-coverage.usecase'; +import { GetAnalysisDriftFreshnessUseCase } from '../application/queries/get-analysis-drift-freshness.usecase'; +import { + mapImpactAnalysisListItem, + mapImpactAnalysisResponse, + mapMergedMultiRepoReportReviewDecision, + mapMultiRepoAnalysisRunDetail, + mapMultiRepoAnalysisRunListItem, + mapReviewDecision, +} from '../infrastructure/impact-analysis.mapper'; + +import { ProjectPermissionService } from '../../project/application/project-permission.service'; + +import { EventLogService } from '../../event-log/application/event-log.service'; + +@Controller('/api/v1') +export class ImpactAnalysisLifecycleController { + constructor( + private readonly createAnalysis: CreateImpactAnalysisUseCase, + private readonly getAnalysis: GetImpactAnalysisUseCase, + private readonly finalizeAnalysis: FinalizeImpactAnalysisUseCase, + private readonly listAnalyses: ListImpactAnalysesUseCase, + private readonly permissions: ProjectPermissionService, + private readonly eventLogService: EventLogService, + ) {} + + @Post('/requirement-revisions/:revisionId/impact-analyses') + async create( + @Param('revisionId') revisionId: string, + @Body() body: unknown, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertPermissionForRequirementRevision( + actor, + revisionId, + 'analysis:create', + ); + const input = impactAnalysisCreateRequestSchema.parse(body); + const analysis = await this.createAnalysis.execute({ + requirementRevisionId: revisionId, + snapshotId: input.snapshotId, + sourceTargetId: input.sourceTargetId, + allowPartialSnapshot: input.allowPartialSnapshot, + requestKey: input.requestKey, + }); + + const response = impactAnalysisResponseSchema.parse( + mapImpactAnalysisResponse({ analysis }), + ); + + return response; + } + + @Get('/impact-analyses/:analysisId') + async get( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const analysis = await this.getAnalysis.execute(analysisId); + return impactAnalysisResponseSchema.parse( + mapImpactAnalysisResponse({ analysis }), + ); + } + + @Get('/impact-analyses/:analysisId/events') + async getEvents( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + // ensure analysis exists is implicitly checked by permission assert if the analysis is missing it will throw 404 + // Wait, assertCanReadAnalysis throws 404 if not found? No, usually project membership is checked. Let's make sure it exists by calling getAnalysis? + // Actually, assertCanReadAnalysis checks ProjectMembership and usually fetches the analysis to verify. + // If not, getting events for a non-existent analysis will just return [] which is fine. + + const events = await this.eventLogService.getAnalysisEvents(analysisId); + return { items: events }; + } + + @Get('/projects/:projectId/analyses') + async list( + @Param('projectId') projectId: string, + @Query() query: unknown, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadProject(actor, projectId); + const parsedQuery = paginationQuerySchema.safeParse(query); + if (!parsedQuery.success) { + throw new BadRequestException(parsedQuery.error.errors); + } + const { limit, offset } = parsedQuery.data; + + const analyses = await this.listAnalyses.execute({ projectId, limit, offset }); + + return impactAnalysisListResponseSchema.parse({ + items: analyses.map((analysis) => mapImpactAnalysisListItem(analysis as unknown as Parameters[0])), + }); + } + + @Post('/impact-analyses/:analysisId/finalize') + async finalize( + @Param('analysisId') analysisId: string, + @Body() body: unknown, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertPermissionForAnalysis( + actor, + analysisId, + 'analysis:finalize', + ); + const input = finalizeImpactAnalysisRequestSchema.parse(body); + const analysis = await this.finalizeAnalysis.execute({ + analysisId, + acknowledgeUnreviewed: input.acknowledgeUnreviewed, + userId: actor.id, + }); + return impactAnalysisResponseSchema.parse( + mapImpactAnalysisResponse({ analysis }), + ); + } +} diff --git a/apps/api/src/modules/impact-analysis/api/impact-analysis.controller.spec.ts b/apps/api/src/modules/impact-analysis/api/impact-analysis-read-model.controller.spec.ts similarity index 65% rename from apps/api/src/modules/impact-analysis/api/impact-analysis.controller.spec.ts rename to apps/api/src/modules/impact-analysis/api/impact-analysis-read-model.controller.spec.ts index 573efbec..b9d4db60 100644 --- a/apps/api/src/modules/impact-analysis/api/impact-analysis.controller.spec.ts +++ b/apps/api/src/modules/impact-analysis/api/impact-analysis-read-model.controller.spec.ts @@ -1,12 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ImpactAnalysisController } from './impact-analysis.controller'; +import { ImpactAnalysisReadModelController } from './impact-analysis-read-model.controller'; import { ProjectPermissionService } from '../../project/application/project-permission.service'; import { GetAnalysisDriftFreshnessUseCase } from '../application/queries/get-analysis-drift-freshness.usecase'; import { UnauthorizedException, NotFoundException } from '@nestjs/common'; import { RequestUser } from '@ba-helper/contracts'; -describe('ImpactAnalysisController - driftFreshness', () => { - let controller: ImpactAnalysisController; +describe('ImpactAnalysisReadModelController - driftFreshness', () => { + let controller: ImpactAnalysisReadModelController; let permissions: jest.Mocked; let getAnalysisDriftFreshness: jest.Mocked; @@ -21,35 +21,15 @@ describe('ImpactAnalysisController - driftFreshness', () => { execute: jest.fn(), } as any; - controller = new ImpactAnalysisController( - null as any, // createAnalysis - null as any, // createMultiRepoAnalyses - null as any, // getAnalysis - null as any, // getMultiRepoRun - null as any, // getMultiRepoImpactMatrix + controller = new ImpactAnalysisReadModelController( null as any, // getMatrixRowDetail - null as any, // getMergedMultiRepoReportDraft - null as any, // finalizeMultiRepoReport - null as any, // getApprovedMultiRepoReport - null as any, // exportApprovedMultiRepoReport - null as any, // listMultiRepoRuns - null as any, // createMergedReportReviewDecision - null as any, // listMergedReportReviewDecisions - null as any, // getLatestMergedReportReviewDecision - null as any, // finalizeAnalysis - null as any, // listAnalyses null as any, // getImpactGraph null as any, // getQaCoverage null as any, // getReviewQueue null as any, // getImpactDiff - null as any, // createReviewDecision - null as any, // listReviewDecisions - null as any, // getLatestReviewDecision null as any, // getLineage - null as any, // getReviewCoverage getAnalysisDriftFreshness, permissions, - null as any, // eventLogService ); }); diff --git a/apps/api/src/modules/impact-analysis/api/impact-analysis-read-model.controller.ts b/apps/api/src/modules/impact-analysis/api/impact-analysis-read-model.controller.ts new file mode 100644 index 00000000..cf626db6 --- /dev/null +++ b/apps/api/src/modules/impact-analysis/api/impact-analysis-read-model.controller.ts @@ -0,0 +1,156 @@ +import { Body, Controller, Get, Param, Post, Query, BadRequestException, NotFoundException, Res } from '@nestjs/common'; +import { + impactAnalysisCreateRequestSchema, + impactAnalysisListResponseSchema, + impactAnalysisResponseSchema, + multiRepoImpactAnalysisCreateRequestSchema, + multiRepoImpactAnalysisCreateResponseSchema, + multiRepoAnalysisRunDetailResponseSchema, + multiRepoAnalysisRunListResponseSchema, + multiRepoImpactMatrixResponseSchema, + multiRepoMergedReportDraftResponseSchema, + multiRepoApprovedReportResponseSchema, + mergedMultiRepoReportReviewDecisionCreateResponseSchema, + mergedMultiRepoReportReviewDecisionListResponseSchema, + mergedMultiRepoReportReviewDecisionResponseSchema, + finalizeImpactAnalysisRequestSchema, + impactGraphResponseSchema, + qaCoverageResponseSchema, + reviewQueueResponseSchema, + paginationQuerySchema, + impactAnalysisDiffResponseSchema, + reviewDecisionRequestSchema, + reviewDecisionCreateResponseSchema, + reviewDecisionListResponseSchema, + reviewDecisionResponseSchema, + lineageTimelineResponseSchema, + driftFreshnessRecommendationSchema, + RequestUser, +} from '@ba-helper/contracts'; +import { CurrentUser } from '../../auth/api/current-user.decorator'; +import { CreateImpactAnalysisUseCase } from '../application/lifecycle/create-impact-analysis.usecase'; +import { CreateMultiRepoImpactAnalysesUseCase } from '../application/multi-repo/create-multi-repo-impact-analyses.usecase'; +import { GetImpactAnalysisUseCase } from '../application/lifecycle/get-impact-analysis.usecase'; +import { GetMultiRepoAnalysisRunUseCase } from '../application/multi-repo/get-multi-repo-analysis-run.usecase'; +import { BuildMultiRepoImpactMatrixReadModel } from '../application/multi-repo/build-multi-repo-impact-matrix.read-model'; +import { GetMatrixRowDetailUseCase } from '../application/queries/get-matrix-row-detail.usecase'; +import { GetMergedMultiRepoReportDraftUseCase } from '../application/multi-repo/get-merged-multi-repo-report-draft.usecase'; +import { FinalizeMultiRepoReportUseCase } from '../application/multi-repo/finalize-multi-repo-report.usecase'; +import { GetApprovedMultiRepoReportUseCase } from '../application/multi-repo/get-approved-multi-repo-report.usecase'; +import { ExportApprovedMultiRepoReportUseCase } from '../application/multi-repo/export-approved-multi-repo-report.usecase'; +import { ListMultiRepoAnalysisRunsUseCase } from '../application/multi-repo/list-multi-repo-analysis-runs.usecase'; +import { CreateMergedMultiRepoReportReviewDecisionUseCase } from '../application/multi-repo/create-merged-multi-repo-report-review-decision.usecase'; +import { ListMergedMultiRepoReportReviewDecisionsUseCase } from '../application/multi-repo/list-merged-multi-repo-report-review-decisions.usecase'; +import { GetLatestMergedMultiRepoReportReviewDecisionUseCase } from '../application/multi-repo/get-latest-merged-multi-repo-report-review-decision.usecase'; +import { FinalizeImpactAnalysisUseCase } from '../application/lifecycle/finalize-impact-analysis.usecase'; +import { ListImpactAnalysesUseCase } from '../application/lifecycle/list-impact-analyses.usecase'; +import { GetImpactGraphUseCase } from '../application/queries/get-impact-graph.usecase'; +import { GetQaCoverageUseCase } from '../application/qa/get-qa-coverage.usecase'; +import { GetReviewQueueUseCase } from '../application/review/get-review-queue.usecase'; +import { GetImpactDiffUseCase } from '../application/queries/get-impact-diff.usecase'; +import { CreateAnalysisReviewDecisionUseCase } from '../application/review/create-analysis-review-decision.usecase'; +import { ListReviewDecisionsUseCase } from '../application/review/list-review-decisions.usecase'; +import { GetLatestReviewDecisionUseCase } from '../application/review/get-latest-review-decision.usecase'; +import { GetImpactAnalysisLineageUseCase } from '../application/queries/get-impact-analysis-lineage.usecase'; +import { GetReviewCoverageUseCase } from '../application/review/get-review-coverage.usecase'; +import { GetAnalysisDriftFreshnessUseCase } from '../application/queries/get-analysis-drift-freshness.usecase'; +import { + mapImpactAnalysisListItem, + mapImpactAnalysisResponse, + mapMergedMultiRepoReportReviewDecision, + mapMultiRepoAnalysisRunDetail, + mapMultiRepoAnalysisRunListItem, + mapReviewDecision, +} from '../infrastructure/impact-analysis.mapper'; + +import { ProjectPermissionService } from '../../project/application/project-permission.service'; + +import { EventLogService } from '../../event-log/application/event-log.service'; + +@Controller('/api/v1') +export class ImpactAnalysisReadModelController { + constructor( + private readonly getMatrixRowDetail: GetMatrixRowDetailUseCase, + private readonly getImpactGraph: GetImpactGraphUseCase, + private readonly getQaCoverage: GetQaCoverageUseCase, + private readonly getReviewQueue: GetReviewQueueUseCase, + private readonly getImpactDiff: GetImpactDiffUseCase, + private readonly getLineage: GetImpactAnalysisLineageUseCase, + private readonly getAnalysisDriftFreshness: GetAnalysisDriftFreshnessUseCase, + private readonly permissions: ProjectPermissionService, + ) {} + + @Get('/multi-repo-runs/:runId/impact-matrix/analyses/:analysisId/details') + async getMatrixRowDetailEndpoint( + @Param('runId') runId: string, + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadMultiRepoRun(actor, runId); + // Extra guard: analysis membership to project is naturally enforced because actor must have access to runId, + // and the use case itself validates that analysisId belongs to runId. + const result = await this.getMatrixRowDetail.execute(runId, analysisId); + return result; // result is already built to schema shape + } + + @Get('/impact-analyses/:analysisId/lineage') + async getLineageTimeline( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const lineage = await this.getLineage.execute(analysisId); + return lineageTimelineResponseSchema.parse(lineage); + } + + @Get('/impact-analyses/:analysisId/graph') + async graph( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const result = await this.getImpactGraph.execute(analysisId); + return impactGraphResponseSchema.parse(result); + } + + @Get('/impact-analyses/:analysisId/qa-coverage') + async qaCoverage( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const result = await this.getQaCoverage.execute(analysisId); + return qaCoverageResponseSchema.parse(result); + } + + @Get('/impact-analyses/:analysisId/review-queue') + async reviewQueue( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const result = await this.getReviewQueue.execute(analysisId); + return reviewQueueResponseSchema.parse(result); + } + + @Get('/impact-analyses/:analysisId/diff') + async diff( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const result = await this.getImpactDiff.execute(analysisId); + return impactAnalysisDiffResponseSchema.parse(result); + } + + @Get('/projects/:projectId/analyses/:analysisId/drift-freshness') + async driftFreshness( + @Param('projectId') projectId: string, + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const result = await this.getAnalysisDriftFreshness.execute(projectId, analysisId); + return driftFreshnessRecommendationSchema.parse(result); + } +} diff --git a/apps/api/src/modules/impact-analysis/api/impact-analysis-review.controller.ts b/apps/api/src/modules/impact-analysis/api/impact-analysis-review.controller.ts new file mode 100644 index 00000000..1e2b3a5b --- /dev/null +++ b/apps/api/src/modules/impact-analysis/api/impact-analysis-review.controller.ts @@ -0,0 +1,141 @@ +import { Body, Controller, Get, Param, Post, Query, BadRequestException, NotFoundException, Res } from '@nestjs/common'; +import { + impactAnalysisCreateRequestSchema, + impactAnalysisListResponseSchema, + impactAnalysisResponseSchema, + multiRepoImpactAnalysisCreateRequestSchema, + multiRepoImpactAnalysisCreateResponseSchema, + multiRepoAnalysisRunDetailResponseSchema, + multiRepoAnalysisRunListResponseSchema, + multiRepoImpactMatrixResponseSchema, + multiRepoMergedReportDraftResponseSchema, + multiRepoApprovedReportResponseSchema, + mergedMultiRepoReportReviewDecisionCreateResponseSchema, + mergedMultiRepoReportReviewDecisionListResponseSchema, + mergedMultiRepoReportReviewDecisionResponseSchema, + finalizeImpactAnalysisRequestSchema, + impactGraphResponseSchema, + qaCoverageResponseSchema, + reviewQueueResponseSchema, + paginationQuerySchema, + impactAnalysisDiffResponseSchema, + reviewDecisionRequestSchema, + reviewDecisionCreateResponseSchema, + reviewDecisionListResponseSchema, + reviewDecisionResponseSchema, + lineageTimelineResponseSchema, + driftFreshnessRecommendationSchema, + RequestUser, +} from '@ba-helper/contracts'; +import { CurrentUser } from '../../auth/api/current-user.decorator'; +import { CreateImpactAnalysisUseCase } from '../application/lifecycle/create-impact-analysis.usecase'; +import { CreateMultiRepoImpactAnalysesUseCase } from '../application/multi-repo/create-multi-repo-impact-analyses.usecase'; +import { GetImpactAnalysisUseCase } from '../application/lifecycle/get-impact-analysis.usecase'; +import { GetMultiRepoAnalysisRunUseCase } from '../application/multi-repo/get-multi-repo-analysis-run.usecase'; +import { BuildMultiRepoImpactMatrixReadModel } from '../application/multi-repo/build-multi-repo-impact-matrix.read-model'; +import { GetMatrixRowDetailUseCase } from '../application/queries/get-matrix-row-detail.usecase'; +import { GetMergedMultiRepoReportDraftUseCase } from '../application/multi-repo/get-merged-multi-repo-report-draft.usecase'; +import { FinalizeMultiRepoReportUseCase } from '../application/multi-repo/finalize-multi-repo-report.usecase'; +import { GetApprovedMultiRepoReportUseCase } from '../application/multi-repo/get-approved-multi-repo-report.usecase'; +import { ExportApprovedMultiRepoReportUseCase } from '../application/multi-repo/export-approved-multi-repo-report.usecase'; +import { ListMultiRepoAnalysisRunsUseCase } from '../application/multi-repo/list-multi-repo-analysis-runs.usecase'; +import { CreateMergedMultiRepoReportReviewDecisionUseCase } from '../application/multi-repo/create-merged-multi-repo-report-review-decision.usecase'; +import { ListMergedMultiRepoReportReviewDecisionsUseCase } from '../application/multi-repo/list-merged-multi-repo-report-review-decisions.usecase'; +import { GetLatestMergedMultiRepoReportReviewDecisionUseCase } from '../application/multi-repo/get-latest-merged-multi-repo-report-review-decision.usecase'; +import { FinalizeImpactAnalysisUseCase } from '../application/lifecycle/finalize-impact-analysis.usecase'; +import { ListImpactAnalysesUseCase } from '../application/lifecycle/list-impact-analyses.usecase'; +import { GetImpactGraphUseCase } from '../application/queries/get-impact-graph.usecase'; +import { GetQaCoverageUseCase } from '../application/qa/get-qa-coverage.usecase'; +import { GetReviewQueueUseCase } from '../application/review/get-review-queue.usecase'; +import { GetImpactDiffUseCase } from '../application/queries/get-impact-diff.usecase'; +import { CreateAnalysisReviewDecisionUseCase } from '../application/review/create-analysis-review-decision.usecase'; +import { ListReviewDecisionsUseCase } from '../application/review/list-review-decisions.usecase'; +import { GetLatestReviewDecisionUseCase } from '../application/review/get-latest-review-decision.usecase'; +import { GetImpactAnalysisLineageUseCase } from '../application/queries/get-impact-analysis-lineage.usecase'; +import { GetReviewCoverageUseCase } from '../application/review/get-review-coverage.usecase'; +import { GetAnalysisDriftFreshnessUseCase } from '../application/queries/get-analysis-drift-freshness.usecase'; +import { + mapImpactAnalysisListItem, + mapImpactAnalysisResponse, + mapMergedMultiRepoReportReviewDecision, + mapMultiRepoAnalysisRunDetail, + mapMultiRepoAnalysisRunListItem, + mapReviewDecision, +} from '../infrastructure/impact-analysis.mapper'; + +import { ProjectPermissionService } from '../../project/application/project-permission.service'; + +import { EventLogService } from '../../event-log/application/event-log.service'; + +@Controller('/api/v1') +export class ImpactAnalysisReviewController { + constructor( + private readonly createReviewDecision: CreateAnalysisReviewDecisionUseCase, + private readonly listReviewDecisions: ListReviewDecisionsUseCase, + private readonly getLatestReviewDecision: GetLatestReviewDecisionUseCase, + private readonly getReviewCoverage: GetReviewCoverageUseCase, + private readonly permissions: ProjectPermissionService, + ) {} + + @Get('/multi-repo-runs/:runId/review-coverage') + async getReviewCoverageEndpoint( + @Param('runId') runId: string, + @CurrentUser() actor: RequestUser, + ) { + // Permission is checked within the use case + const result = await this.getReviewCoverage.execute(actor, runId); + return result; // result is already validated by contract schema format implicitly, or we can use reviewCoverageResponseSchema.parse + } + + @Post('/impact-analyses/:analysisId/review-decisions') + async createReviewDecisionEndpoint( + @Param('analysisId') analysisId: string, + @Body() body: unknown, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertPermissionForAnalysis( + actor, + analysisId, + 'review:write', + ); + const input = reviewDecisionRequestSchema.parse(body); + + const result = await this.createReviewDecision.execute({ + analysisId, + decision: input.decision, + note: input.note, + actor, + }); + + return reviewDecisionCreateResponseSchema.parse({ + decision: mapReviewDecision(result.decision), + reportRegenerated: result.reportRegenerated, + reportRegenerationError: result.reportRegenerationError, + }); + } + + @Get('/impact-analyses/:analysisId/review-decisions') + async listReviewDecisionsEndpoint( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const result = await this.listReviewDecisions.execute(analysisId); + return reviewDecisionListResponseSchema.parse({ + items: result.items.map(mapReviewDecision), + }); + } + + @Get('/impact-analyses/:analysisId/review-decisions/latest') + async getLatestReviewDecisionEndpoint( + @Param('analysisId') analysisId: string, + @CurrentUser() actor: RequestUser, + ) { + await this.permissions.assertCanReadAnalysis(actor, analysisId); + const result = await this.getLatestReviewDecision.execute(analysisId); + if (!result) { + throw new NotFoundException('No review decisions found for this analysis.'); + } + return reviewDecisionResponseSchema.parse(mapReviewDecision(result)); + } +} diff --git a/apps/api/src/modules/impact-analysis/api/impact-analysis.controller.ts b/apps/api/src/modules/impact-analysis/api/multi-repo-analysis.controller.ts similarity index 55% rename from apps/api/src/modules/impact-analysis/api/impact-analysis.controller.ts rename to apps/api/src/modules/impact-analysis/api/multi-repo-analysis.controller.ts index b76c8bb7..f89348d1 100644 --- a/apps/api/src/modules/impact-analysis/api/impact-analysis.controller.ts +++ b/apps/api/src/modules/impact-analysis/api/multi-repo-analysis.controller.ts @@ -68,14 +68,11 @@ import { ProjectPermissionService } from '../../project/application/project-perm import { EventLogService } from '../../event-log/application/event-log.service'; @Controller('/api/v1') -export class ImpactAnalysisController { +export class MultiRepoAnalysisController { constructor( - private readonly createAnalysis: CreateImpactAnalysisUseCase, private readonly createMultiRepoAnalyses: CreateMultiRepoImpactAnalysesUseCase, - private readonly getAnalysis: GetImpactAnalysisUseCase, private readonly getMultiRepoRun: GetMultiRepoAnalysisRunUseCase, private readonly getMultiRepoImpactMatrix: BuildMultiRepoImpactMatrixReadModel, - private readonly getMatrixRowDetail: GetMatrixRowDetailUseCase, private readonly getMergedMultiRepoReportDraft: GetMergedMultiRepoReportDraftUseCase, private readonly finalizeMultiRepoReport: FinalizeMultiRepoReportUseCase, private readonly getApprovedMultiRepoReport: GetApprovedMultiRepoReportUseCase, @@ -84,49 +81,9 @@ export class ImpactAnalysisController { private readonly createMergedReportReviewDecision: CreateMergedMultiRepoReportReviewDecisionUseCase, private readonly listMergedReportReviewDecisions: ListMergedMultiRepoReportReviewDecisionsUseCase, private readonly getLatestMergedReportReviewDecision: GetLatestMergedMultiRepoReportReviewDecisionUseCase, - private readonly finalizeAnalysis: FinalizeImpactAnalysisUseCase, - private readonly listAnalyses: ListImpactAnalysesUseCase, - private readonly getImpactGraph: GetImpactGraphUseCase, - private readonly getQaCoverage: GetQaCoverageUseCase, - private readonly getReviewQueue: GetReviewQueueUseCase, - private readonly getImpactDiff: GetImpactDiffUseCase, - private readonly createReviewDecision: CreateAnalysisReviewDecisionUseCase, - private readonly listReviewDecisions: ListReviewDecisionsUseCase, - private readonly getLatestReviewDecision: GetLatestReviewDecisionUseCase, - private readonly getLineage: GetImpactAnalysisLineageUseCase, - private readonly getReviewCoverage: GetReviewCoverageUseCase, - private readonly getAnalysisDriftFreshness: GetAnalysisDriftFreshnessUseCase, private readonly permissions: ProjectPermissionService, - private readonly eventLogService: EventLogService, ) {} - @Post('/requirement-revisions/:revisionId/impact-analyses') - async create( - @Param('revisionId') revisionId: string, - @Body() body: unknown, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertPermissionForRequirementRevision( - actor, - revisionId, - 'analysis:create', - ); - const input = impactAnalysisCreateRequestSchema.parse(body); - const analysis = await this.createAnalysis.execute({ - requirementRevisionId: revisionId, - snapshotId: input.snapshotId, - sourceTargetId: input.sourceTargetId, - allowPartialSnapshot: input.allowPartialSnapshot, - requestKey: input.requestKey, - }); - - const response = impactAnalysisResponseSchema.parse( - mapImpactAnalysisResponse({ analysis }), - ); - - return response; - } - @Post('/projects/:projectId/multi-repo-analyses') async createMultiRepo( @Param('projectId') projectId: string, @@ -164,16 +121,6 @@ export class ImpactAnalysisController { ); } - @Get('/multi-repo-runs/:runId/review-coverage') - async getReviewCoverageEndpoint( - @Param('runId') runId: string, - @CurrentUser() actor: RequestUser, - ) { - // Permission is checked within the use case - const result = await this.getReviewCoverage.execute(actor, runId); - return result; // result is already validated by contract schema format implicitly, or we can use reviewCoverageResponseSchema.parse - } - @Get('/multi-repo-runs/:runId/impact-matrix') async getMultiRepoRunImpactMatrix( @Param('runId') runId: string, @@ -184,19 +131,6 @@ export class ImpactAnalysisController { return multiRepoImpactMatrixResponseSchema.parse(result); } - @Get('/multi-repo-runs/:runId/impact-matrix/analyses/:analysisId/details') - async getMatrixRowDetailEndpoint( - @Param('runId') runId: string, - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadMultiRepoRun(actor, runId); - // Extra guard: analysis membership to project is naturally enforced because actor must have access to runId, - // and the use case itself validates that analysisId belongs to runId. - const result = await this.getMatrixRowDetail.execute(runId, analysisId); - return result; // result is already built to schema shape - } - @Get('/multi-repo-runs/:runId/merged-report-draft') async getMergedMultiRepoReportDraftEndpoint( @Param('runId') runId: string, @@ -340,186 +274,4 @@ export class ImpactAnalysisController { items: runs.map((run) => mapMultiRepoAnalysisRunListItem(run)), }); } - - @Get('/impact-analyses/:analysisId') - async get( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const analysis = await this.getAnalysis.execute(analysisId); - return impactAnalysisResponseSchema.parse( - mapImpactAnalysisResponse({ analysis }), - ); - } - - @Get('/impact-analyses/:analysisId/events') - async getEvents( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - // ensure analysis exists is implicitly checked by permission assert if the analysis is missing it will throw 404 - // Wait, assertCanReadAnalysis throws 404 if not found? No, usually project membership is checked. Let's make sure it exists by calling getAnalysis? - // Actually, assertCanReadAnalysis checks ProjectMembership and usually fetches the analysis to verify. - // If not, getting events for a non-existent analysis will just return [] which is fine. - - const events = await this.eventLogService.getAnalysisEvents(analysisId); - return { items: events }; - } - - @Get('/impact-analyses/:analysisId/lineage') - async getLineageTimeline( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const lineage = await this.getLineage.execute(analysisId); - return lineageTimelineResponseSchema.parse(lineage); - } - - @Get('/projects/:projectId/analyses') - async list( - @Param('projectId') projectId: string, - @Query() query: unknown, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadProject(actor, projectId); - const parsedQuery = paginationQuerySchema.safeParse(query); - if (!parsedQuery.success) { - throw new BadRequestException(parsedQuery.error.errors); - } - const { limit, offset } = parsedQuery.data; - - const analyses = await this.listAnalyses.execute({ projectId, limit, offset }); - - return impactAnalysisListResponseSchema.parse({ - items: analyses.map((analysis) => mapImpactAnalysisListItem(analysis as unknown as Parameters[0])), - }); - } - - @Post('/impact-analyses/:analysisId/finalize') - async finalize( - @Param('analysisId') analysisId: string, - @Body() body: unknown, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertPermissionForAnalysis( - actor, - analysisId, - 'analysis:finalize', - ); - const input = finalizeImpactAnalysisRequestSchema.parse(body); - const analysis = await this.finalizeAnalysis.execute({ - analysisId, - acknowledgeUnreviewed: input.acknowledgeUnreviewed, - userId: actor.id, - }); - return impactAnalysisResponseSchema.parse( - mapImpactAnalysisResponse({ analysis }), - ); - } - - @Get('/impact-analyses/:analysisId/graph') - async graph( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const result = await this.getImpactGraph.execute(analysisId); - return impactGraphResponseSchema.parse(result); - } - - @Get('/impact-analyses/:analysisId/qa-coverage') - async qaCoverage( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const result = await this.getQaCoverage.execute(analysisId); - return qaCoverageResponseSchema.parse(result); - } - - @Get('/impact-analyses/:analysisId/review-queue') - async reviewQueue( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const result = await this.getReviewQueue.execute(analysisId); - return reviewQueueResponseSchema.parse(result); - } - - @Get('/impact-analyses/:analysisId/diff') - async diff( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const result = await this.getImpactDiff.execute(analysisId); - return impactAnalysisDiffResponseSchema.parse(result); - } - - @Get('/projects/:projectId/analyses/:analysisId/drift-freshness') - async driftFreshness( - @Param('projectId') projectId: string, - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const result = await this.getAnalysisDriftFreshness.execute(projectId, analysisId); - return driftFreshnessRecommendationSchema.parse(result); - } - - @Post('/impact-analyses/:analysisId/review-decisions') - async createReviewDecisionEndpoint( - @Param('analysisId') analysisId: string, - @Body() body: unknown, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertPermissionForAnalysis( - actor, - analysisId, - 'review:write', - ); - const input = reviewDecisionRequestSchema.parse(body); - - const result = await this.createReviewDecision.execute({ - analysisId, - decision: input.decision, - note: input.note, - actor, - }); - - return reviewDecisionCreateResponseSchema.parse({ - decision: mapReviewDecision(result.decision), - reportRegenerated: result.reportRegenerated, - reportRegenerationError: result.reportRegenerationError, - }); - } - - @Get('/impact-analyses/:analysisId/review-decisions') - async listReviewDecisionsEndpoint( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const result = await this.listReviewDecisions.execute(analysisId); - return reviewDecisionListResponseSchema.parse({ - items: result.items.map(mapReviewDecision), - }); - } - - @Get('/impact-analyses/:analysisId/review-decisions/latest') - async getLatestReviewDecisionEndpoint( - @Param('analysisId') analysisId: string, - @CurrentUser() actor: RequestUser, - ) { - await this.permissions.assertCanReadAnalysis(actor, analysisId); - const result = await this.getLatestReviewDecision.execute(analysisId); - if (!result) { - throw new NotFoundException('No review decisions found for this analysis.'); - } - return reviewDecisionResponseSchema.parse(mapReviewDecision(result)); - } } diff --git a/apps/api/src/modules/impact-analysis/impact-analysis.module.ts b/apps/api/src/modules/impact-analysis/impact-analysis.module.ts index e633db69..d2731864 100644 --- a/apps/api/src/modules/impact-analysis/impact-analysis.module.ts +++ b/apps/api/src/modules/impact-analysis/impact-analysis.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { ImpactAnalysisController } from './api/impact-analysis.controller'; import { CreateImpactAnalysisUseCase } from './application/lifecycle/create-impact-analysis.usecase'; import { GetImpactAnalysisUseCase } from './application/lifecycle/get-impact-analysis.usecase'; import { FinalizeImpactAnalysisUseCase } from './application/lifecycle/finalize-impact-analysis.usecase'; @@ -39,6 +38,10 @@ import { GetLatestMergedMultiRepoReportReviewDecisionUseCase } from './applicati import { MergedMultiRepoReportDraftBuilder } from './application/multi-repo/merged-multi-repo-report-draft.builder'; import { ReviewNoteController } from './api/review-note.controller'; import { ReviewClarificationController } from './api/review-clarification.controller'; +import { ImpactAnalysisLifecycleController } from './api/impact-analysis-lifecycle.controller'; +import { ImpactAnalysisReadModelController } from './api/impact-analysis-read-model.controller'; +import { ImpactAnalysisReviewController } from './api/impact-analysis-review.controller'; +import { MultiRepoAnalysisController } from './api/multi-repo-analysis.controller'; import { ReviewNoteRepository } from './infrastructure/review-note.repository'; import { ReviewDecisionRepository } from './infrastructure/review-decision.repository'; import { ReviewClarificationRepository } from './infrastructure/review-clarification.repository'; @@ -75,7 +78,14 @@ import { DomainPackModule } from '../domain-pack/domain-pack.module'; @Module({ imports: [PrismaModule, EventLogModule, DocumentModule, QueueModule, AiModule, RetrievalModule, GraphModule, ClarificationModule, ProjectModule, RepositoryModule, DomainPackModule], - controllers: [ImpactAnalysisController, ReviewNoteController, ReviewClarificationController], + controllers: [ + ImpactAnalysisLifecycleController, + ImpactAnalysisReadModelController, + ImpactAnalysisReviewController, + MultiRepoAnalysisController, + ReviewNoteController, + ReviewClarificationController, + ], providers: [ ImpactAnalysisRepository, MultiRepoAnalysisRunRepository, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b819b7c4..60ff86b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: ts-jest: specifier: ^29.4.11 version: 29.4.11(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.40)(@types/node@25.9.1)(typescript@5.4.5)))(typescript@5.4.5) + ts-morph: + specifier: 28.0.0 + version: 28.0.0 tsx: specifier: ^4.22.3 version: 4.22.3 @@ -10356,7 +10359,7 @@ snapshots: '@next/eslint-plugin-next': 16.2.6 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -10379,7 +10382,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -10394,14 +10397,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -10416,7 +10419,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.4.5))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.3 is-core-module: 2.16.2 is-glob: 4.0.3 From 8d72b7170d6fa199aa434a38dca46c93d7c9786a Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 07:57:37 +0700 Subject: [PATCH 05/10] refactor(web): centralize query keys --- apps/web/src/hooks/api/use-analyses.ts | 2 +- apps/web/src/hooks/api/use-clarifications.ts | 8 ++++---- .../src/hooks/api/use-final-reviewed-report.ts | 3 ++- apps/web/src/hooks/api/use-impact-matrix.ts | 4 ++-- apps/web/src/hooks/api/use-insights.ts | 6 +++--- apps/web/src/hooks/api/use-repositories.ts | 8 +------- .../web/src/hooks/api/use-review-completion.ts | 3 ++- apps/web/src/hooks/api/use-review-decisions.ts | 4 ++-- apps/web/src/hooks/api/use-review-notes.ts | 7 ++++--- .../hooks/api/use-reviewed-report-snapshot.ts | 3 ++- apps/web/src/hooks/api/use-traceability.ts | 4 ++-- apps/web/src/lib/api/query-keys.ts | 18 ++++++++++++++++++ 12 files changed, 43 insertions(+), 27 deletions(-) diff --git a/apps/web/src/hooks/api/use-analyses.ts b/apps/web/src/hooks/api/use-analyses.ts index e7c7ae53..00b942ac 100644 --- a/apps/web/src/hooks/api/use-analyses.ts +++ b/apps/web/src/hooks/api/use-analyses.ts @@ -79,7 +79,7 @@ export * from "./use-reports" export function useAnalysisLineage(analysisId: string) { return useQuery({ - queryKey: [...queryKeys.analyses.detail(analysisId), "lineage"], + queryKey: queryKeys.analyses.lineage(analysisId), queryFn: async () => { const { lineageTimelineResponseSchema } = await import("@ba-helper/contracts") return apiGet( diff --git a/apps/web/src/hooks/api/use-clarifications.ts b/apps/web/src/hooks/api/use-clarifications.ts index 6c037c04..a39b349b 100644 --- a/apps/web/src/hooks/api/use-clarifications.ts +++ b/apps/web/src/hooks/api/use-clarifications.ts @@ -113,7 +113,7 @@ export function useConvertClarification(analysisId: string) { export function useReviewClarifications(analysisId: string) { return useQuery({ - queryKey: [...queryKeys.analyses.detail(analysisId), "review-clarifications"], + queryKey: queryKeys.analyses.reviewClarifications(analysisId), queryFn: async () => { const { reviewClarificationListResponseSchema } = await import("@ba-helper/contracts") return apiGet( @@ -139,7 +139,7 @@ export function useCreateReviewClarification(analysisId: string) { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [...queryKeys.analyses.detail(analysisId), "review-clarifications"], + queryKey: queryKeys.analyses.reviewClarifications(analysisId), }) }, }) @@ -159,7 +159,7 @@ export function useAnswerReviewClarification(analysisId: string) { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [...queryKeys.analyses.detail(analysisId), "review-clarifications"], + queryKey: queryKeys.analyses.reviewClarifications(analysisId), }) }, }) @@ -180,7 +180,7 @@ export function useCreateDerivedAnalysisFromClarification(analysisId: string) { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [...queryKeys.analyses.detail(analysisId), "review-clarifications"], + queryKey: queryKeys.analyses.reviewClarifications(analysisId), }) queryClient.invalidateQueries({ queryKey: queryKeys.analyses.list(activeProjectId ?? "__workspace-pending__"), diff --git a/apps/web/src/hooks/api/use-final-reviewed-report.ts b/apps/web/src/hooks/api/use-final-reviewed-report.ts index cd6b78de..6eb3ebd3 100644 --- a/apps/web/src/hooks/api/use-final-reviewed-report.ts +++ b/apps/web/src/hooks/api/use-final-reviewed-report.ts @@ -1,10 +1,11 @@ +import { queryKeys } from "@/lib/api/query-keys" import { useQuery } from "@tanstack/react-query" import { apiGet } from "@/lib/api-client" import { finalReviewedReportResponseSchema } from "@ba-helper/contracts" export function useFinalReviewedReport(analysisId: string, options?: { enabled?: boolean }) { return useQuery({ - queryKey: ["final-reviewed-report", analysisId], + queryKey: queryKeys.documents.finalReviewedReport(analysisId), queryFn: () => apiGet( `/api/v1/impact-analyses/${analysisId}/final-reviewed-report`, diff --git a/apps/web/src/hooks/api/use-impact-matrix.ts b/apps/web/src/hooks/api/use-impact-matrix.ts index 65a63987..c149b7e1 100644 --- a/apps/web/src/hooks/api/use-impact-matrix.ts +++ b/apps/web/src/hooks/api/use-impact-matrix.ts @@ -4,7 +4,7 @@ import { queryKeys } from "@/lib/api/query-keys" export function useMultiRepoImpactMatrix(runId: string) { return useQuery({ - queryKey: [...queryKeys.analyses.runs.detail(runId), "impact-matrix"], + queryKey: queryKeys.analyses.runs.impactMatrix(runId), queryFn: async () => { const { multiRepoImpactMatrixResponseSchema } = await import("@ba-helper/contracts") return apiGet( @@ -19,7 +19,7 @@ export function useMultiRepoImpactMatrix(runId: string) { export function useMatrixRowDetail(runId: string, analysisId: string | null) { return useQuery({ - queryKey: ["multi-repo-run", runId, "impact-matrix-row-detail", analysisId], + queryKey: queryKeys.analyses.runs.impactMatrixRowDetail(runId, analysisId), queryFn: async () => { if (!analysisId) throw new Error("analysisId is required") const { matrixRowDetailResponseSchema } = await import("@ba-helper/contracts") diff --git a/apps/web/src/hooks/api/use-insights.ts b/apps/web/src/hooks/api/use-insights.ts index 2d58631c..a0c1563d 100644 --- a/apps/web/src/hooks/api/use-insights.ts +++ b/apps/web/src/hooks/api/use-insights.ts @@ -13,7 +13,7 @@ import { export function useAnalysisInsights(analysisId: string) { return useQuery({ - queryKey: [...queryKeys.analyses.detail(analysisId), "insights"], + queryKey: queryKeys.analyses.insights(analysisId), queryFn: async () => { return apiGet(`/api/v1/impact-analyses/${analysisId}/insights`, insightListResponseSchema) }, @@ -23,7 +23,7 @@ export function useAnalysisInsights(analysisId: string) { export function useAnalysisTraceability(analysisId: string) { return useQuery({ - queryKey: [...queryKeys.analyses.detail(analysisId), "traceability"], + queryKey: queryKeys.analyses.traceability(analysisId), queryFn: async () => { return apiGet(`/api/v1/impact-analyses/${analysisId}/traceability`, traceabilityLinkListResponseSchema) }, @@ -43,7 +43,7 @@ export function useReviewInsight(projectId: string | undefined, analysisId: stri }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [...queryKeys.analyses.detail(analysisId), "insights"], + queryKey: queryKeys.analyses.insights(analysisId), }) queryClient.invalidateQueries({ queryKey: queryKeys.analyses.detail(analysisId), diff --git a/apps/web/src/hooks/api/use-repositories.ts b/apps/web/src/hooks/api/use-repositories.ts index f8b77187..3c0f256d 100644 --- a/apps/web/src/hooks/api/use-repositories.ts +++ b/apps/web/src/hooks/api/use-repositories.ts @@ -87,13 +87,7 @@ export function useSnapshotDrift( ) return useQuery({ - queryKey: [ - ...queryKeys.repositories.detail(repositoryId ?? ''), - "snapshots", - baseSnapshotId, - "drift", - targetCommitSha, - ], + queryKey: queryKeys.repositories.snapshotDrift(repositoryId || "", baseSnapshotId || "", targetCommitSha), queryFn: async () => { const url = new URL( `/api/v1/projects/${effectiveProjectId}/repositories/${repositoryId}/snapshots/${baseSnapshotId}/drift`, diff --git a/apps/web/src/hooks/api/use-review-completion.ts b/apps/web/src/hooks/api/use-review-completion.ts index 537b6ded..d7fc46b0 100644 --- a/apps/web/src/hooks/api/use-review-completion.ts +++ b/apps/web/src/hooks/api/use-review-completion.ts @@ -1,10 +1,11 @@ +import { queryKeys } from "@/lib/api/query-keys" import { useQuery } from "@tanstack/react-query" import { apiGet } from "@/lib/api-client" import { reviewCompletionResponseSchema } from "@ba-helper/contracts" export function useReviewCompletion(analysisId: string) { return useQuery({ - queryKey: ["review-completion", analysisId], + queryKey: queryKeys.analyses.reviewCompletion(analysisId), queryFn: () => apiGet( `/api/v1/impact-analyses/${analysisId}/review-completion`, diff --git a/apps/web/src/hooks/api/use-review-decisions.ts b/apps/web/src/hooks/api/use-review-decisions.ts index ddb17953..bc83bb24 100644 --- a/apps/web/src/hooks/api/use-review-decisions.ts +++ b/apps/web/src/hooks/api/use-review-decisions.ts @@ -17,7 +17,7 @@ import { export function useReviewDecisions(analysisId: string) { return useQuery({ - queryKey: [...queryKeys.analyses.detail(analysisId), "review-decisions"], + queryKey: queryKeys.analyses.reviewDecisions(analysisId), queryFn: async () => { return apiGet( `/api/v1/impact-analyses/${analysisId}/review-decisions`, @@ -41,7 +41,7 @@ export function useCreateReviewDecision(analysisId: string) { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [...queryKeys.analyses.detail(analysisId), "review-decisions"], + queryKey: queryKeys.analyses.reviewDecisions(analysisId), }) queryClient.invalidateQueries({ queryKey: queryKeys.analyses.detail(analysisId), diff --git a/apps/web/src/hooks/api/use-review-notes.ts b/apps/web/src/hooks/api/use-review-notes.ts index 874d6d1a..0c62b5b8 100644 --- a/apps/web/src/hooks/api/use-review-notes.ts +++ b/apps/web/src/hooks/api/use-review-notes.ts @@ -1,10 +1,11 @@ +import { queryKeys } from "@/lib/api/query-keys" import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { CreateReviewNoteRequest, ReviewNoteResponse } from '@ba-helper/contracts'; import { apiGet, apiPost } from '@/lib/api-client'; export function useReviewNotes(analysisId: string) { return useQuery({ - queryKey: ['impact-analyses', analysisId, 'review-notes'], + queryKey: queryKeys.analyses.reviewNotes(analysisId), queryFn: async (): Promise<{ items: ReviewNoteResponse[] }> => { // Assuming apiGet returns the payload directly and handles errors return apiGet<{ items: ReviewNoteResponse[] }>(`/api/v1/impact-analyses/${analysisId}/review-notes`); @@ -21,9 +22,9 @@ export function useSaveReviewNote(analysisId: string) { return apiPost(`/api/v1/impact-analyses/${analysisId}/review-notes`, req); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['impact-analyses', analysisId, 'review-notes'] }); + queryClient.invalidateQueries({ queryKey: queryKeys.analyses.reviewNotes(analysisId) }); // Invalidate review queue if necessary - queryClient.invalidateQueries({ queryKey: ['impact-analyses', analysisId, 'review-queue'] }); + queryClient.invalidateQueries({ queryKey: queryKeys.analyses.reviewQueue(analysisId) }); }, }); } diff --git a/apps/web/src/hooks/api/use-reviewed-report-snapshot.ts b/apps/web/src/hooks/api/use-reviewed-report-snapshot.ts index 4cc39c67..0938e091 100644 --- a/apps/web/src/hooks/api/use-reviewed-report-snapshot.ts +++ b/apps/web/src/hooks/api/use-reviewed-report-snapshot.ts @@ -1,3 +1,4 @@ +import { queryKeys } from "@/lib/api/query-keys" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { apiGet, apiPost } from "@/lib/api-client" import { ReviewedReportSnapshotResponse } from "@ba-helper/contracts" @@ -6,7 +7,7 @@ const SNAPSHOT_QUERY_KEY = "reviewed-report-snapshot" export function useLatestReviewedReportSnapshot(analysisId: string | undefined) { return useQuery({ - queryKey: [SNAPSHOT_QUERY_KEY, analysisId], + queryKey: queryKeys.documents.reviewedReportSnapshot(analysisId), queryFn: async () => { if (!analysisId) throw new Error("Analysis ID is required") const result = await apiGet(`/api/v1/impact-analyses/${analysisId}/reviewed-report-snapshot/latest`) diff --git a/apps/web/src/hooks/api/use-traceability.ts b/apps/web/src/hooks/api/use-traceability.ts index 662deafc..0054f995 100644 --- a/apps/web/src/hooks/api/use-traceability.ts +++ b/apps/web/src/hooks/api/use-traceability.ts @@ -28,7 +28,7 @@ export function useUpdateTraceabilityReviewDecision(analysisId: string, linkId: queryKey: queryKeys.analyses.detail(analysisId), }) queryClient.invalidateQueries({ - queryKey: [...queryKeys.analyses.detail(analysisId), "traceability"], + queryKey: queryKeys.analyses.traceability(analysisId), }) }, }) @@ -51,7 +51,7 @@ export function useDeleteTraceabilityReviewDecision(analysisId: string, linkId: queryKey: queryKeys.analyses.detail(analysisId), }) queryClient.invalidateQueries({ - queryKey: [...queryKeys.analyses.detail(analysisId), "traceability"], + queryKey: queryKeys.analyses.traceability(analysisId), }) }, }) diff --git a/apps/web/src/lib/api/query-keys.ts b/apps/web/src/lib/api/query-keys.ts index bf30d577..54ef4438 100644 --- a/apps/web/src/lib/api/query-keys.ts +++ b/apps/web/src/lib/api/query-keys.ts @@ -31,6 +31,8 @@ export const queryKeys = { reviewDecisions: (runId: string) => ["impact-analyses", "runs", "review-decisions", runId] as const, latestReviewDecision: (runId: string) => ["impact-analyses", "runs", "latest-review-decision", runId] as const, reviewCoverage: (runId: string) => ["impact-analyses", "runs", "review-coverage", runId] as const, + impactMatrix: (runId: string) => ["impact-analyses", "runs", "impact-matrix", runId] as const, + impactMatrixRowDetail: (runId: string, analysisId: string) => ["impact-analyses", "runs", "impact-matrix-row-detail", runId, analysisId] as const, }, report: (analysisId: string) => ["impact-analyses", "approved-report", analysisId] as const, graph: (analysisId: string) => ["impact-analyses", "graph", analysisId] as const, @@ -38,5 +40,21 @@ export const queryKeys = { reviewQueue: (analysisId: string) => ["impact-analyses", "review-queue", analysisId] as const, diff: (analysisId: string) => ["impact-analyses", "diff", analysisId] as const, driftFreshness: (analysisId: string) => ["impact-analyses", "drift-freshness", analysisId] as const, + lineage: (analysisId: string) => ["impact-analyses", "lineage", analysisId] as const, + reviewNotes: (analysisId: string) => ["impact-analyses", "review-notes", analysisId] as const, + reviewDecisions: (analysisId: string) => ["impact-analyses", "review-decisions", analysisId] as const, + reviewClarifications: (analysisId: string) => ["impact-analyses", "review-clarifications", analysisId] as const, + reviewCompletion: (analysisId: string) => ["impact-analyses", "review-completion", analysisId] as const, + traceability: (analysisId: string) => ["impact-analyses", "traceability", analysisId] as const, + insights: (analysisId: string) => ["impact-analyses", "insights", analysisId] as const, + }, + documents: { + finalReviewedReport: (analysisId: string) => ["documents", analysisId, "final-reviewed-report"] as const, + documentJobs: (analysisId: string) => ["documents", analysisId, "document-jobs"] as const, + reviewedReportSnapshot: (analysisId: string) => ["documents", analysisId, "reviewed-report-snapshot"] as const, + }, + eventLogs: { + analysis: (analysisId: string) => ["event-logs", "analysis", analysisId] as const, + scanJob: (repositoryId: string, jobId: string) => ["event-logs", "repository", repositoryId, "scan-job", jobId] as const, }, } From d9d27d330a0c9748afa0f4b21146062ec841162f Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 07:59:01 +0700 Subject: [PATCH 06/10] refactor(web): organize workspace components --- .../components/workspace/analysis/affected-artifact-card.tsx | 2 +- .../workspace/analysis/impact-analysis-workspace.tsx | 2 +- .../workspace/{shared => analysis}/insight/insight-card.tsx | 0 .../{shared => analysis}/insight/insight-filter-bar.tsx | 0 .../workspace/{shared => analysis}/insight/insight-list.tsx | 2 +- .../workspace/{shared => analysis}/qa/qa-coverage-badge.tsx | 0 .../workspace/{shared => analysis}/qa/qa-coverage-panel.tsx | 0 .../{shared => analysis}/retrieval/code-evidence-block.tsx | 4 ++-- .../{shared => analysis}/retrieval/evidence-inspector.tsx | 0 .../{shared => analysis}/retrieval/retrieval-signals.tsx | 0 .../{shared => analysis}/retrieval/retrieval-suggestion.tsx | 0 .../src/components/workspace/matrix/matrix-evidence-list.tsx | 2 +- 12 files changed, 6 insertions(+), 6 deletions(-) rename apps/web/src/components/workspace/{shared => analysis}/insight/insight-card.tsx (100%) rename apps/web/src/components/workspace/{shared => analysis}/insight/insight-filter-bar.tsx (100%) rename apps/web/src/components/workspace/{shared => analysis}/insight/insight-list.tsx (93%) rename apps/web/src/components/workspace/{shared => analysis}/qa/qa-coverage-badge.tsx (100%) rename apps/web/src/components/workspace/{shared => analysis}/qa/qa-coverage-panel.tsx (100%) rename apps/web/src/components/workspace/{shared => analysis}/retrieval/code-evidence-block.tsx (97%) rename apps/web/src/components/workspace/{shared => analysis}/retrieval/evidence-inspector.tsx (100%) rename apps/web/src/components/workspace/{shared => analysis}/retrieval/retrieval-signals.tsx (100%) rename apps/web/src/components/workspace/{shared => analysis}/retrieval/retrieval-suggestion.tsx (100%) diff --git a/apps/web/src/components/workspace/analysis/affected-artifact-card.tsx b/apps/web/src/components/workspace/analysis/affected-artifact-card.tsx index a2d647b8..04bc876a 100644 --- a/apps/web/src/components/workspace/analysis/affected-artifact-card.tsx +++ b/apps/web/src/components/workspace/analysis/affected-artifact-card.tsx @@ -1,5 +1,5 @@ import { TraceabilityLinkListResponse } from "@ba-helper/contracts" -import { RetrievalSignalBadge } from "@/components/workspace/shared/retrieval/retrieval-signals" +import { RetrievalSignalBadge } from "@/components/workspace/analysis/retrieval/retrieval-signals" type TraceabilityLink = TraceabilityLinkListResponse["items"][number] diff --git a/apps/web/src/components/workspace/analysis/impact-analysis-workspace.tsx b/apps/web/src/components/workspace/analysis/impact-analysis-workspace.tsx index 07de0484..f947c927 100644 --- a/apps/web/src/components/workspace/analysis/impact-analysis-workspace.tsx +++ b/apps/web/src/components/workspace/analysis/impact-analysis-workspace.tsx @@ -1,5 +1,5 @@ import { ReactNode } from "react" -import { EvidenceInspector } from "@/components/workspace/shared/retrieval/evidence-inspector" +import { EvidenceInspector } from "@/components/workspace/analysis/retrieval/evidence-inspector" import { MousePointerClick } from "lucide-react" import { Group, Panel, Separator } from "react-resizable-panels" import { useMediaQuery } from "@/hooks/ui/use-media-query" diff --git a/apps/web/src/components/workspace/shared/insight/insight-card.tsx b/apps/web/src/components/workspace/analysis/insight/insight-card.tsx similarity index 100% rename from apps/web/src/components/workspace/shared/insight/insight-card.tsx rename to apps/web/src/components/workspace/analysis/insight/insight-card.tsx diff --git a/apps/web/src/components/workspace/shared/insight/insight-filter-bar.tsx b/apps/web/src/components/workspace/analysis/insight/insight-filter-bar.tsx similarity index 100% rename from apps/web/src/components/workspace/shared/insight/insight-filter-bar.tsx rename to apps/web/src/components/workspace/analysis/insight/insight-filter-bar.tsx diff --git a/apps/web/src/components/workspace/shared/insight/insight-list.tsx b/apps/web/src/components/workspace/analysis/insight/insight-list.tsx similarity index 93% rename from apps/web/src/components/workspace/shared/insight/insight-list.tsx rename to apps/web/src/components/workspace/analysis/insight/insight-list.tsx index daef8775..fb9cd63c 100644 --- a/apps/web/src/components/workspace/shared/insight/insight-list.tsx +++ b/apps/web/src/components/workspace/analysis/insight/insight-list.tsx @@ -1,5 +1,5 @@ import { InsightListResponse } from "@ba-helper/contracts" -import { InsightCard } from "@/components/workspace/shared/insight/insight-card" +import { InsightCard } from "@/components/workspace/analysis/insight/insight-card" type Insight = InsightListResponse["items"][number] diff --git a/apps/web/src/components/workspace/shared/qa/qa-coverage-badge.tsx b/apps/web/src/components/workspace/analysis/qa/qa-coverage-badge.tsx similarity index 100% rename from apps/web/src/components/workspace/shared/qa/qa-coverage-badge.tsx rename to apps/web/src/components/workspace/analysis/qa/qa-coverage-badge.tsx diff --git a/apps/web/src/components/workspace/shared/qa/qa-coverage-panel.tsx b/apps/web/src/components/workspace/analysis/qa/qa-coverage-panel.tsx similarity index 100% rename from apps/web/src/components/workspace/shared/qa/qa-coverage-panel.tsx rename to apps/web/src/components/workspace/analysis/qa/qa-coverage-panel.tsx diff --git a/apps/web/src/components/workspace/shared/retrieval/code-evidence-block.tsx b/apps/web/src/components/workspace/analysis/retrieval/code-evidence-block.tsx similarity index 97% rename from apps/web/src/components/workspace/shared/retrieval/code-evidence-block.tsx rename to apps/web/src/components/workspace/analysis/retrieval/code-evidence-block.tsx index b493424f..1f046f0e 100644 --- a/apps/web/src/components/workspace/shared/retrieval/code-evidence-block.tsx +++ b/apps/web/src/components/workspace/analysis/retrieval/code-evidence-block.tsx @@ -3,8 +3,8 @@ import { useState, useCallback } from "react" import { Code2, Copy, Check } from "lucide-react" -import { RetrievalSignalBadge, RetrievalReason, RetrievalDebugPanel } from "@/components/workspace/shared/retrieval/retrieval-signals" -import { RetrievalSuggestion } from "@/components/workspace/shared/retrieval/retrieval-suggestion" +import { RetrievalSignalBadge, RetrievalReason, RetrievalDebugPanel } from "@/components/workspace/analysis/retrieval/retrieval-signals" +import { RetrievalSuggestion } from "@/components/workspace/analysis/retrieval/retrieval-suggestion" import { RetrievalMetadata } from "@ba-helper/contracts" // Basic syntax highlighting for TS/JS diff --git a/apps/web/src/components/workspace/shared/retrieval/evidence-inspector.tsx b/apps/web/src/components/workspace/analysis/retrieval/evidence-inspector.tsx similarity index 100% rename from apps/web/src/components/workspace/shared/retrieval/evidence-inspector.tsx rename to apps/web/src/components/workspace/analysis/retrieval/evidence-inspector.tsx diff --git a/apps/web/src/components/workspace/shared/retrieval/retrieval-signals.tsx b/apps/web/src/components/workspace/analysis/retrieval/retrieval-signals.tsx similarity index 100% rename from apps/web/src/components/workspace/shared/retrieval/retrieval-signals.tsx rename to apps/web/src/components/workspace/analysis/retrieval/retrieval-signals.tsx diff --git a/apps/web/src/components/workspace/shared/retrieval/retrieval-suggestion.tsx b/apps/web/src/components/workspace/analysis/retrieval/retrieval-suggestion.tsx similarity index 100% rename from apps/web/src/components/workspace/shared/retrieval/retrieval-suggestion.tsx rename to apps/web/src/components/workspace/analysis/retrieval/retrieval-suggestion.tsx diff --git a/apps/web/src/components/workspace/matrix/matrix-evidence-list.tsx b/apps/web/src/components/workspace/matrix/matrix-evidence-list.tsx index 0748529d..0dbaa1c6 100644 --- a/apps/web/src/components/workspace/matrix/matrix-evidence-list.tsx +++ b/apps/web/src/components/workspace/matrix/matrix-evidence-list.tsx @@ -1,5 +1,5 @@ import { MatrixRowArtifactDetail } from "@ba-helper/contracts" -import { CodeEvidenceBlock } from "@/components/workspace/shared/retrieval/code-evidence-block" +import { CodeEvidenceBlock } from "@/components/workspace/analysis/retrieval/code-evidence-block" interface MatrixEvidenceListProps { artifact: MatrixRowArtifactDetail From 8e71445c49f54fabba67bbd2fbe1e32d919b075d Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 08:02:10 +0700 Subject: [PATCH 07/10] refactor(web): align api hooks by domain --- apps/web/src/hooks/api/use-analyses.ts | 52 ++++++++++++++++++- apps/web/src/hooks/api/use-analysis-diff.ts | 18 ------- apps/web/src/hooks/api/use-documents.ts | 4 ++ apps/web/src/hooks/api/use-event-logs.ts | 9 ++-- .../hooks/api/use-final-reviewed-report.ts | 17 ------ apps/web/src/hooks/api/use-impact-graph.ts | 17 ------ apps/web/src/hooks/api/use-review-queue.ts | 16 ------ .../hooks/api/use-reviewed-report-snapshot.ts | 33 ------------ apps/web/src/hooks/ui/use-status-watcher.ts | 4 +- 9 files changed, 61 insertions(+), 109 deletions(-) delete mode 100644 apps/web/src/hooks/api/use-analysis-diff.ts create mode 100644 apps/web/src/hooks/api/use-documents.ts delete mode 100644 apps/web/src/hooks/api/use-final-reviewed-report.ts delete mode 100644 apps/web/src/hooks/api/use-impact-graph.ts delete mode 100644 apps/web/src/hooks/api/use-review-queue.ts delete mode 100644 apps/web/src/hooks/api/use-reviewed-report-snapshot.ts diff --git a/apps/web/src/hooks/api/use-analyses.ts b/apps/web/src/hooks/api/use-analyses.ts index 00b942ac..f8f3a0c5 100644 --- a/apps/web/src/hooks/api/use-analyses.ts +++ b/apps/web/src/hooks/api/use-analyses.ts @@ -1,6 +1,17 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { apiGet, apiPost } from "@/lib/api-client" import { queryKeys } from "@/lib/api/query-keys" +import { canPollAnalysisDetail } from "@/lib/status-helpers" +import { useOptionalProjectId } from "@/lib/project-context" +import { useQuery } from "@tanstack/react-query" +import { apiGet } from "@/lib/api-client" +import { impactGraphResponseSchema, ImpactGraphResponse } from "@ba-helper/contracts" +import { useQuery } from '@tanstack/react-query' +import { apiGet } from '@/lib/api-client' +import { queryKeys } from '@/lib/api/query-keys' +import { ReviewQueueResponse, reviewQueueResponseSchema } from '@ba-helper/contracts' +import { ImpactAnalysisDiffResponse, impactAnalysisDiffResponseSchema } from "@ba-helper/contracts" + import { ImpactAnalysisListResponse, ImpactAnalysisDetailResponse, @@ -10,8 +21,6 @@ import { impactAnalysisResponseSchema, } from "@ba-helper/contracts" -import { canPollAnalysisDetail } from "@/lib/status-helpers" -import { useOptionalProjectId } from "@/lib/project-context" export function useAnalyses(params?: { projectId?: string; limit?: number; offset?: number }) { const activeProjectId = useOptionalProjectId() @@ -104,3 +113,42 @@ export function useAnalysisDriftFreshness(projectId: string | undefined, analysi enabled: Boolean(projectId && analysisId), }) } + +export function useImpactGraph(analysisId: string | undefined, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: queryKeys.analyses.graph(analysisId ?? ""), + queryFn: () => + apiGet( + `/api/v1/impact-analyses/${analysisId}/graph`, + impactGraphResponseSchema, + ), + enabled: Boolean(analysisId) && (options?.enabled ?? true), + staleTime: 30_000, + }) +} + +export function useReviewQueue(analysisId: string | undefined, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: queryKeys.analyses.reviewQueue(analysisId ?? ''), + queryFn: () => + apiGet( + `/api/v1/impact-analyses/${analysisId}/review-queue`, + reviewQueueResponseSchema, + ), + enabled: Boolean(analysisId) && (options?.enabled ?? true), + }) +} + +export function useAnalysisDiff(analysisId: string, enabled: boolean = true) { + return useQuery({ + queryKey: queryKeys.analyses.diff(analysisId), + queryFn: async () => { + return apiGet( + `/api/v1/impact-analyses/${analysisId}/diff`, + impactAnalysisDiffResponseSchema + ) + }, + enabled: Boolean(analysisId) && enabled, + refetchOnWindowFocus: true, + }) +} diff --git a/apps/web/src/hooks/api/use-analysis-diff.ts b/apps/web/src/hooks/api/use-analysis-diff.ts deleted file mode 100644 index e5186afa..00000000 --- a/apps/web/src/hooks/api/use-analysis-diff.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useQuery } from "@tanstack/react-query" -import { apiGet } from "@/lib/api-client" -import { queryKeys } from "@/lib/api/query-keys" -import { ImpactAnalysisDiffResponse, impactAnalysisDiffResponseSchema } from "@ba-helper/contracts" - -export function useAnalysisDiff(analysisId: string, enabled: boolean = true) { - return useQuery({ - queryKey: queryKeys.analyses.diff(analysisId), - queryFn: async () => { - return apiGet( - `/api/v1/impact-analyses/${analysisId}/diff`, - impactAnalysisDiffResponseSchema - ) - }, - enabled: Boolean(analysisId) && enabled, - refetchOnWindowFocus: true, - }) -} diff --git a/apps/web/src/hooks/api/use-documents.ts b/apps/web/src/hooks/api/use-documents.ts new file mode 100644 index 00000000..aaa11cef --- /dev/null +++ b/apps/web/src/hooks/api/use-documents.ts @@ -0,0 +1,4 @@ +import { ReviewedReportSnapshotResponse } from "@ba-helper/contracts" +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '@/lib/api-client'; +import { queryKeys } from '@/lib/api/query-keys'; diff --git a/apps/web/src/hooks/api/use-event-logs.ts b/apps/web/src/hooks/api/use-event-logs.ts index 8cb25b17..9ee185e8 100644 --- a/apps/web/src/hooks/api/use-event-logs.ts +++ b/apps/web/src/hooks/api/use-event-logs.ts @@ -1,3 +1,4 @@ +import { queryKeys } from "@/lib/api/query-keys"; import { useQuery } from '@tanstack/react-query'; import { apiGet } from '@/lib/api-client'; import { EventLogListResponse } from '@ba-helper/contracts'; @@ -9,9 +10,9 @@ export const eventLogKeys = { analysis: (analysisId: string) => [...eventLogKeys.all, 'analysis', analysisId] as const, }; -export function useScanJobEvents(repositoryId: string, jobId: string | undefined) { +export function useScanJobEventLogs(repositoryId: string, jobId: string | undefined) { return useQuery({ - queryKey: eventLogKeys.scanJob(jobId || ''), + queryKey: queryKeys.eventLogs.scanJob(repositoryId, jobId || ''), queryFn: async () => { const data = await apiGet( `/api/v1/repositories/${repositoryId}/scan-jobs/${jobId}/events` @@ -22,9 +23,9 @@ export function useScanJobEvents(repositoryId: string, jobId: string | undefined }); } -export function useAnalysisEvents(analysisId: string | undefined) { +export function useAnalysisEventLogs(analysisId: string | undefined) { return useQuery({ - queryKey: eventLogKeys.analysis(analysisId || ''), + queryKey: queryKeys.eventLogs.analysis(analysisId || ''), queryFn: async () => { const data = await apiGet( `/api/v1/impact-analyses/${analysisId}/events` diff --git a/apps/web/src/hooks/api/use-final-reviewed-report.ts b/apps/web/src/hooks/api/use-final-reviewed-report.ts deleted file mode 100644 index 6eb3ebd3..00000000 --- a/apps/web/src/hooks/api/use-final-reviewed-report.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { queryKeys } from "@/lib/api/query-keys" -import { useQuery } from "@tanstack/react-query" -import { apiGet } from "@/lib/api-client" -import { finalReviewedReportResponseSchema } from "@ba-helper/contracts" - -export function useFinalReviewedReport(analysisId: string, options?: { enabled?: boolean }) { - return useQuery({ - queryKey: queryKeys.documents.finalReviewedReport(analysisId), - queryFn: () => - apiGet( - `/api/v1/impact-analyses/${analysisId}/final-reviewed-report`, - finalReviewedReportResponseSchema, - ), - enabled: options?.enabled, - retry: false, // Do not retry if the gate blocks it - }) -} diff --git a/apps/web/src/hooks/api/use-impact-graph.ts b/apps/web/src/hooks/api/use-impact-graph.ts deleted file mode 100644 index 757e8b69..00000000 --- a/apps/web/src/hooks/api/use-impact-graph.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from "@tanstack/react-query" -import { apiGet } from "@/lib/api-client" -import { queryKeys } from "@/lib/api/query-keys" -import { impactGraphResponseSchema, ImpactGraphResponse } from "@ba-helper/contracts" - -export function useImpactGraph(analysisId: string | undefined, options?: { enabled?: boolean }) { - return useQuery({ - queryKey: queryKeys.analyses.graph(analysisId ?? ""), - queryFn: () => - apiGet( - `/api/v1/impact-analyses/${analysisId}/graph`, - impactGraphResponseSchema, - ), - enabled: Boolean(analysisId) && (options?.enabled ?? true), - staleTime: 30_000, - }) -} diff --git a/apps/web/src/hooks/api/use-review-queue.ts b/apps/web/src/hooks/api/use-review-queue.ts deleted file mode 100644 index 94749b6c..00000000 --- a/apps/web/src/hooks/api/use-review-queue.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { apiGet } from '@/lib/api-client' -import { queryKeys } from '@/lib/api/query-keys' -import { ReviewQueueResponse, reviewQueueResponseSchema } from '@ba-helper/contracts' - -export function useReviewQueue(analysisId: string | undefined, options?: { enabled?: boolean }) { - return useQuery({ - queryKey: queryKeys.analyses.reviewQueue(analysisId ?? ''), - queryFn: () => - apiGet( - `/api/v1/impact-analyses/${analysisId}/review-queue`, - reviewQueueResponseSchema, - ), - enabled: Boolean(analysisId) && (options?.enabled ?? true), - }) -} diff --git a/apps/web/src/hooks/api/use-reviewed-report-snapshot.ts b/apps/web/src/hooks/api/use-reviewed-report-snapshot.ts deleted file mode 100644 index 0938e091..00000000 --- a/apps/web/src/hooks/api/use-reviewed-report-snapshot.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { queryKeys } from "@/lib/api/query-keys" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { apiGet, apiPost } from "@/lib/api-client" -import { ReviewedReportSnapshotResponse } from "@ba-helper/contracts" - -const SNAPSHOT_QUERY_KEY = "reviewed-report-snapshot" - -export function useLatestReviewedReportSnapshot(analysisId: string | undefined) { - return useQuery({ - queryKey: queryKeys.documents.reviewedReportSnapshot(analysisId), - queryFn: async () => { - if (!analysisId) throw new Error("Analysis ID is required") - const result = await apiGet(`/api/v1/impact-analyses/${analysisId}/reviewed-report-snapshot/latest`) - return result as ReviewedReportSnapshotResponse - }, - enabled: !!analysisId, - retry: false, // Don't retry if 404 (no snapshot yet) - }) -} - -export function useCreateReviewedReportSnapshot(analysisId: string) { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async () => { - const result = await apiPost(`/api/v1/impact-analyses/${analysisId}/reviewed-report-snapshot`, {}) - return result as ReviewedReportSnapshotResponse - }, - onSuccess: (data) => { - queryClient.setQueryData([SNAPSHOT_QUERY_KEY, analysisId], data) - }, - }) -} diff --git a/apps/web/src/hooks/ui/use-status-watcher.ts b/apps/web/src/hooks/ui/use-status-watcher.ts index 23b9777d..50d98bb0 100644 --- a/apps/web/src/hooks/ui/use-status-watcher.ts +++ b/apps/web/src/hooks/ui/use-status-watcher.ts @@ -67,8 +67,8 @@ export function useAnalysisStatusWatcher(projectId: string | undefined, analysis if (currentStatus === "WAITING_FOR_REVIEW") { toast.success("Analysis ready for review.", { id: toastId }) // Invalidate review related queries - queryClient.invalidateQueries({ queryKey: [...queryKeys.analyses.detail(analysisId), "insights"] }) - queryClient.invalidateQueries({ queryKey: [...queryKeys.analyses.detail(analysisId), "traceability"] }) + queryClient.invalidateQueries({ queryKey: queryKeys.analyses.insights(analysisId) }) + queryClient.invalidateQueries({ queryKey: queryKeys.analyses.traceability(analysisId) }) } else if (currentStatus === "COMPLETED") { toast.success("Analysis finalized.", { id: toastId }) queryClient.invalidateQueries({ queryKey: queryKeys.analyses.report(analysisId) }) From 9832b125a8a39413bbc85343cbeb51048c8cc7e6 Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 08:33:55 +0700 Subject: [PATCH 08/10] docs: align portfolio story with audit workflow --- README.md | 7 +++-- docs/portfolio/case-study.md | 55 +++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 60ea93da..1f659968 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Our analysis is strictly constrained to prevent hallucinations and fabricated cl - **No AI in Final Export:** The final markdown report is generated strictly from the frozen database payload, with zero active LLM calls or retrieval processes during the export phase. ## 5. Demo Workflow -The primary golden path demo validates the core evidence-first pipeline (`scan → impact analysis → evidence → review → report → drift visibility`). +The primary golden path demo validates the core evidence-first pipeline. The complete audited workflow involves: +`scan → impact analysis → evidence → review → snapshot → async report → drift → rerun lineage`. You can run the definitive automated integration test for the focused TypeScript/NestJS demo path: @@ -36,7 +37,7 @@ pnpm demo:golden-path ``` **Visual Case Study:** -For a step-by-step visual walkthrough of this workflow, see the [Demo Case Study](docs/portfolio/case-study.md). +For a step-by-step visual walkthrough of this workflow, see the [Demo Case Study](docs/portfolio/case-study.md), which features an 8-screen proof pack demonstrating the full end-to-end audit and lifecycle process. **Sample Requirement:** > "When a paid booking is cancelled, the system must refund the tenant, prevent double refunds, update booking/payment state, and notify relevant parties." @@ -229,7 +230,7 @@ Built as a TypeScript modular monolith to balance speed of development with even - **Primary demo stack:** TypeScript/NestJS is the strongest and `STABLE` scanner path. - **Pilot scanner adapters:** Java/Spring Boot is `PARTIAL`; Go `net/http`, Go/Gin, Python/FastAPI, C#/ASP.NET Core, PHP/Laravel, and Ruby/Rails are `EXPERIMENTAL` capability proofs. - **Capability metadata:** Every scan exposes `SCANNER_CAPABILITY_SUMMARY` so reviewers can see whether a result came from a `STABLE`, `PARTIAL`, or `EXPERIMENTAL` adapter. -- **Output generation:** Impact matrices, QA scenarios, unknown/risk tracking, human review, Markdown/PDF exports, and drift-aware traceability reports. +- **Output generation:** Impact matrices, QA scenarios, unknown/risk tracking, human review gates, deterministic snapshot-sourced Markdown/PDF exports, and drift-aware lineage reports. ## Known Limits - TypeScript/NestJS is the strongest scanner path. diff --git a/docs/portfolio/case-study.md b/docs/portfolio/case-study.md index ba5f2d73..cab69387 100644 --- a/docs/portfolio/case-study.md +++ b/docs/portfolio/case-study.md @@ -2,53 +2,62 @@ This case study visually demonstrates the end-to-end workflow of BA Helper and shows how it enforces an audit-style traceability process from raw requirement to reviewed final report. -## 1. The Problem +## 1. Problem When business logic changes, Technical BAs and QA engineers historically rely on manual codebase searches or tribal knowledge to map backend impacts. This process is brittle, un-auditable, and prone to costly QA regressions. ## 2. Requirement Change We inject a typical, high-risk business requirement: > "When a booking is cancelled after payment, the system must release room inventory, mark the booking as cancelled, and prevent duplicate refund requests." -The system consumes this text alongside a static snapshot of the backend repository. +The system consumes this text alongside a static snapshot of the backend repository to prepare for impact analysis. -![Analysis workspace](./assets/01-analysis-workspace.png) +## 3. Repository Scan + Graph +The backend parses the AST and generates a dependency graph. This provides a structural map of the codebase before any AI inference begins. -## 3. Analysis Output -The backend parses the AST and generates a dependency graph, proposing likely impacted backend artifacts. -Expected impacted artifacts for the demo include `BookingService`, `InventoryService`, `RefundController`, or equivalent modules depending on the analyzed repository structure. +![Repository scan + graph edges](./assets/01-repository-scan-graph.png) -![Impacted artifacts](./assets/02-impacted-artifacts.png) +## 4. Evidence-backed Impact Analysis +BA Helper does not fabricate claims. Every proposed impact is strictly tethered to code-level evidence. The Evidence Inspector provides the exact file path and code excerpts justifying the impact. -## 4. Evidence and Traceability -BA Helper does not hallucinate. Every proposed impact is strictly tethered to code-level evidence. The Evidence Inspector provides the exact file path and line numbers justifying the impact. +![Evidence-backed impact analysis](./assets/02-evidence-backed-impact-analysis.png) -![Evidence inspector](./assets/03-evidence-inspector.png) +## 5. Audit Timeline +Every significant action—from the initial scan to analysis creation, human review, and finalization—is recorded in an immutable audit timeline, ensuring full historical traceability. -The Evidence Quality Table allows the analyst to audit the precision of the LLM's retrieval. +![Audit timeline](./assets/03-audit-timeline.png) -![Evidence quality table](./assets/04-evidence-quality-table.png) +## 6. Human Review Gate +The system enforces a strict human-in-the-loop workflow. The analyst must transition every traceability link to a validated decision (e.g., `ACCEPTED`, `REJECTED`, or `NEEDS_MORE_EVIDENCE`). -## 5. Human Review -The system enforces a strict human-in-the-loop workflow. The analyst must transition every traceability link to a validated decision (`ACCEPTED`, `REJECTED`, `NEEDS_MORE_EVIDENCE`). +![Human review gate](./assets/04-human-review-gate.png) -![Human review decisions](./assets/05-human-review-decisions.png) +## 7. Reviewed Snapshot +Once the review is 100% complete, the analyst triggers a snapshot. This locks the decisions and evidence into an append-only state, securing the historical record against future code changes. -## 6. Immutable Snapshot -Once the review is 100% complete, the analyst triggers a snapshot. This performs a deep copy of the decisions and evidence, locking them into an append-only state in the database. +![Reviewed snapshot viewer](./assets/05-reviewed-snapshot-viewer.png) -![Locked snapshot](./assets/06-locked-snapshot.png) +## 8. Async Final Report Generation +With the snapshot locked, an asynchronous document job is enqueued to generate a deterministic final Markdown report directly from the frozen database payload, without using any active LLM calls. -## 7. Final Reviewed Export -With 0 unreviewed links and a locked snapshot, the deterministic review gate unlocks. The system generates a final Markdown export directly from the frozen snapshot, completely bypassing any live AI generation. +![Async document job status + final report](./assets/06-async-document-job-final-report.png) -![Final review gate and export](./assets/07-final-review-gate-export.png) +## 9. Snapshot Drift +As the underlying codebase evolves, the system calculates drift. If newer commits diverge from the frozen snapshot, a warning is raised to highlight that the analysis may be stale. -## 8. Tested Guarantees +![Snapshot drift drawer](./assets/07-snapshot-drift-drawer.png) + +## 10. Re-analysis Lineage / Diff +When a new analysis is triggered due to drift or requirement updates, the system tracks the lineage between the old and new analysis, highlighting changed artifacts, unknowns, and QA scenarios. + +![Re-run analysis lineage / diff](./assets/08-rerun-lineage-diff.png) + +## 11. Tested Guarantees This audit workflow is not just conceptual; it is strictly enforced by our CI/CD pipeline: - **E17A Backend Tests:** Asserts that the API blocks exports if a snapshot is missing or links are unreviewed. - **E17B Frontend Tests:** Asserts the UI correctly disables export functions and serves deterministic Blob downloads when the gate is passed. -## 9. Known Limits +## 12. Known Limits - The system is an impact analyzer, not a formal verification engine. - AI proposals are hints; human review is always required. - Current deep parser support is optimized for TypeScript/NestJS repositories. +- Pilot language adapters demonstrate extraction contracts, not full compiler-level semantic analysis. From 0f4c5fce996bacd6fc4b94743fbca82e37d50217 Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 08:35:23 +0700 Subject: [PATCH 09/10] refactor(web): update import paths for workspace components and domain hooks --- .../analyses/[analysisId]/_components/analysis-diff-tab.tsx | 2 +- .../_components/analysis-evidence-inspector.tsx | 2 +- .../[analysisId]/_components/analysis-insights-tab.tsx | 4 ++-- .../analyses/[analysisId]/_hooks/use-analysis-workspace.ts | 6 +++--- apps/web/src/app/(app)/analyses/[analysisId]/page.tsx | 6 +++--- apps/web/src/app/(app)/repositories/[repositoryId]/page.tsx | 4 ++-- apps/web/src/components/graph/impact-graph-inspector.tsx | 4 ++-- .../src/components/report/final-reviewed-report-viewer.tsx | 2 +- apps/web/src/components/report/reviewed-snapshot-panel.tsx | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-diff-tab.tsx b/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-diff-tab.tsx index 94f32acb..2b1b6e73 100644 --- a/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-diff-tab.tsx +++ b/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-diff-tab.tsx @@ -1,6 +1,6 @@ "use client" -import { useAnalysisDiff } from "@/hooks/api/use-analysis-diff" +import { useAnalysisDiff } from "@/hooks/api/use-analyses" import { useReviewDecisions } from "@/hooks/api/use-review-decisions" import { ImpactAnalysisDetailResponse } from "@ba-helper/contracts" import { Loader2, AlertCircle, RefreshCw } from "lucide-react" diff --git a/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-evidence-inspector.tsx b/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-evidence-inspector.tsx index bbd24a1f..04c7afaa 100644 --- a/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-evidence-inspector.tsx +++ b/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-evidence-inspector.tsx @@ -1,6 +1,6 @@ "use client" -import { CodeEvidenceBlock } from "@/components/workspace/shared/retrieval/code-evidence-block" +import { CodeEvidenceBlock } from "@/components/workspace/analysis/retrieval/code-evidence-block" import { ImpactGraphInspector } from "@/components/graph/impact-graph-inspector" import { AlertCircle, FileCode2 } from "lucide-react" import { ClarificationWidget } from "@/components/workspace/analysis/clarification/clarification-widget" diff --git a/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-insights-tab.tsx b/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-insights-tab.tsx index b92381a4..80fd8bd7 100644 --- a/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-insights-tab.tsx +++ b/apps/web/src/app/(app)/analyses/[analysisId]/_components/analysis-insights-tab.tsx @@ -1,8 +1,8 @@ "use client" -import { InsightList } from "@/components/workspace/shared/insight/insight-list" +import { InsightList } from "@/components/workspace/analysis/insight/insight-list" import { AffectedArtifactCard } from "@/components/workspace/analysis/affected-artifact-card" -import { InsightFilterBar, type InsightFilterValue } from "@/components/workspace/shared/insight/insight-filter-bar" +import { InsightFilterBar, type InsightFilterValue } from "@/components/workspace/analysis/insight/insight-filter-bar" import { Button } from "@/components/ui/button" import type { InsightListResponse, TraceabilityLinkListResponse } from "@ba-helper/contracts" diff --git a/apps/web/src/app/(app)/analyses/[analysisId]/_hooks/use-analysis-workspace.ts b/apps/web/src/app/(app)/analyses/[analysisId]/_hooks/use-analysis-workspace.ts index 30b3594a..88f4f71c 100644 --- a/apps/web/src/app/(app)/analyses/[analysisId]/_hooks/use-analysis-workspace.ts +++ b/apps/web/src/app/(app)/analyses/[analysisId]/_hooks/use-analysis-workspace.ts @@ -10,14 +10,14 @@ import { useReviewTraceabilityLink, useCreateAnalysis } from "@/hooks/api/use-analyses" -import { useImpactGraph } from "@/hooks/api/use-impact-graph" +import { useImpactGraph } from "@/hooks/api/use-analyses" import { useQaCoverage } from "@/hooks/api/use-qa-coverage" -import { useReviewQueue } from "@/hooks/api/use-review-queue" +import { useReviewQueue } from "@/hooks/api/use-analyses" import { useCurrentWorkspace, useOptionalProjectId } from "@/lib/project-context" import { canFinalizeAnalysis, canReview as canReviewPermission, canRunAnalysis, canViewReviewQueue } from "@/lib/permissions" import { queryKeys } from "@/lib/api/query-keys" import { InsightListResponse, TraceabilityLinkListResponse, ImpactGraphNode } from "@ba-helper/contracts" -import { type InsightFilterValue } from "@/components/workspace/shared/insight/insight-filter-bar" +import { type InsightFilterValue } from "@/components/workspace/analysis/insight/insight-filter-bar" type Insight = InsightListResponse["items"][number] type TraceabilityLink = TraceabilityLinkListResponse["items"][number] diff --git a/apps/web/src/app/(app)/analyses/[analysisId]/page.tsx b/apps/web/src/app/(app)/analyses/[analysisId]/page.tsx index 4bf7cd90..2a44a289 100644 --- a/apps/web/src/app/(app)/analyses/[analysisId]/page.tsx +++ b/apps/web/src/app/(app)/analyses/[analysisId]/page.tsx @@ -8,7 +8,7 @@ import { AlertCircle, Network } from "lucide-react" import { AnalysisProgress } from "@/components/workspace/analysis/analysis-progress" import { isAnalysisActive } from "@/lib/status-helpers" import dynamic from "next/dynamic" -import { QaCoveragePanel } from "@/components/workspace/shared/qa/qa-coverage-panel" +import { QaCoveragePanel } from "@/components/workspace/analysis/qa/qa-coverage-panel" import { ReviewQueuePanel } from "@/components/workspace/review/review-queue-panel" import { AnalysisTabBar } from "./_components/analysis-tab-bar" import { AnalysisInsightsTab } from "./_components/analysis-insights-tab" @@ -21,7 +21,7 @@ import { AnalysisInspectorMapper } from "./_components/analysis-inspector-mapper import { toast } from "sonner" import { v4 as uuidv4 } from "uuid" import { AuditTimeline } from "@/components/workspace/shared/audit-timeline" -import { useAnalysisEvents } from "@/hooks/api/use-event-logs" +import { useAnalysisEventLogs } from "@/hooks/api/use-event-logs" // Dynamic import so React Flow CSS loads correctly in Next.js app router const ImpactGraphView = dynamic( @@ -36,7 +36,7 @@ export default function ImpactAnalysisDetailPage({ }) { const { analysisId } = use(params) const ws = useAnalysisWorkspace(analysisId) - const { data: analysisEventsData, isLoading: isLoadingEvents } = useAnalysisEvents(analysisId) + const { data: analysisEventsData, isLoading: isLoadingEvents } = useAnalysisEventLogs(analysisId) // ── Loading state ── if (ws.analysisLoading || ws.insightsLoading || ws.linksLoading) { diff --git a/apps/web/src/app/(app)/repositories/[repositoryId]/page.tsx b/apps/web/src/app/(app)/repositories/[repositoryId]/page.tsx index e1259e52..065b523a 100644 --- a/apps/web/src/app/(app)/repositories/[repositoryId]/page.tsx +++ b/apps/web/src/app/(app)/repositories/[repositoryId]/page.tsx @@ -20,7 +20,7 @@ import { DiagnosticItem } from "@ba-helper/contracts" import { Skeleton } from "@/components/ui/skeleton" import { v4 as uuidv4 } from "uuid" -import { useScanJobEvents } from "@/hooks/api/use-event-logs" +import { useScanJobEventLogs } from "@/hooks/api/use-event-logs" import { AuditTimeline } from "@/components/workspace/shared/audit-timeline" import { RepositorySnapshotBanner } from "./_components/repository-snapshot-banner" import { RepositoryScannerProfile } from "./_components/repository-scanner-profile" @@ -42,7 +42,7 @@ export default function RepositoryDetailsPage({ params }: PageProps) { const workspace = useCurrentWorkspace() const canScan = workspace ? canRunScan(workspace.membershipRole) : false - const { data: scanEventsData, isLoading: isLoadingEvents } = useScanJobEvents(repositoryId, repo?.latestScanJob?.id) + const { data: scanEventsData, isLoading: isLoadingEvents } = useScanJobEventLogs(repositoryId, repo?.latestScanJob?.id) // Watch for scan job completion/failure to show toast notifications useRepositoryStatusWatcher(undefined, repositoryId) diff --git a/apps/web/src/components/graph/impact-graph-inspector.tsx b/apps/web/src/components/graph/impact-graph-inspector.tsx index 27171dce..f68d38c3 100644 --- a/apps/web/src/components/graph/impact-graph-inspector.tsx +++ b/apps/web/src/components/graph/impact-graph-inspector.tsx @@ -2,10 +2,10 @@ import { ImpactGraphNode } from "@ba-helper/contracts" import { X } from "lucide-react" -import { RetrievalSuggestion } from "@/components/workspace/shared/retrieval/retrieval-suggestion" +import { RetrievalSuggestion } from "@/components/workspace/analysis/retrieval/retrieval-suggestion" import { QaCoverageItem } from "@ba-helper/contracts" -import { QaCoverageBadge } from "@/components/workspace/shared/qa/qa-coverage-badge" +import { QaCoverageBadge } from "@/components/workspace/analysis/qa/qa-coverage-badge" interface Props { node: ImpactGraphNode diff --git a/apps/web/src/components/report/final-reviewed-report-viewer.tsx b/apps/web/src/components/report/final-reviewed-report-viewer.tsx index 40dbcab5..0db19b21 100644 --- a/apps/web/src/components/report/final-reviewed-report-viewer.tsx +++ b/apps/web/src/components/report/final-reviewed-report-viewer.tsx @@ -1,4 +1,4 @@ -import { useFinalReviewedReport } from "@/hooks/api/use-final-reviewed-report" +import { useFinalReviewedReport } from "@/hooks/api/use-documents" import { Dialog, DialogContent, diff --git a/apps/web/src/components/report/reviewed-snapshot-panel.tsx b/apps/web/src/components/report/reviewed-snapshot-panel.tsx index b08fa72b..2ba73755 100644 --- a/apps/web/src/components/report/reviewed-snapshot-panel.tsx +++ b/apps/web/src/components/report/reviewed-snapshot-panel.tsx @@ -1,5 +1,5 @@ import { useState } from "react" -import { useLatestReviewedReportSnapshot, useCreateReviewedReportSnapshot } from "@/hooks/api/use-reviewed-report-snapshot" +import { useLatestReviewedReportSnapshot, useCreateReviewedReportSnapshot } from "@/hooks/api/use-documents" import { Button } from "@/components/ui/button" import { Lock, Loader2, CheckCircle, FileText } from "lucide-react" import { LockedSnapshotViewer } from "./locked-snapshot-viewer" From da56be48faa079e2ea5ba37f9a2d70d25fff266e Mon Sep 17 00:00:00 2001 From: Dip Hung Thinh Date: Wed, 24 Jun 2026 10:02:48 +0700 Subject: [PATCH 10/10] fix(web): resolve build errors from missing hooks and duplicate imports --- apps/web/src/hooks/api/use-analyses.ts | 11 ++--- apps/web/src/hooks/api/use-documents.ts | 50 +++++++++++++++++++-- apps/web/src/hooks/api/use-impact-matrix.ts | 2 +- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/apps/web/src/hooks/api/use-analyses.ts b/apps/web/src/hooks/api/use-analyses.ts index f8f3a0c5..7c2f306f 100644 --- a/apps/web/src/hooks/api/use-analyses.ts +++ b/apps/web/src/hooks/api/use-analyses.ts @@ -3,12 +3,7 @@ import { apiGet, apiPost } from "@/lib/api-client" import { queryKeys } from "@/lib/api/query-keys" import { canPollAnalysisDetail } from "@/lib/status-helpers" import { useOptionalProjectId } from "@/lib/project-context" -import { useQuery } from "@tanstack/react-query" -import { apiGet } from "@/lib/api-client" -import { impactGraphResponseSchema, ImpactGraphResponse } from "@ba-helper/contracts" -import { useQuery } from '@tanstack/react-query' -import { apiGet } from '@/lib/api-client' -import { queryKeys } from '@/lib/api/query-keys' + import { ReviewQueueResponse, reviewQueueResponseSchema } from '@ba-helper/contracts' import { ImpactAnalysisDiffResponse, impactAnalysisDiffResponseSchema } from "@ba-helper/contracts" @@ -19,6 +14,8 @@ import { ImpactAnalysisResponse, impactAnalysisListResponseSchema, impactAnalysisResponseSchema, + impactGraphResponseSchema, + ImpactGraphResponse, } from "@ba-helper/contracts" @@ -78,7 +75,7 @@ export function useCreateAnalysis(projectId?: string) { }) } -export * from "./use-analysis-diff" + export * from "./use-multi-repo-runs" export * from "./use-impact-matrix" export * from "./use-insights" diff --git a/apps/web/src/hooks/api/use-documents.ts b/apps/web/src/hooks/api/use-documents.ts index aaa11cef..be304198 100644 --- a/apps/web/src/hooks/api/use-documents.ts +++ b/apps/web/src/hooks/api/use-documents.ts @@ -1,4 +1,46 @@ -import { ReviewedReportSnapshotResponse } from "@ba-helper/contracts" -import { useQuery } from '@tanstack/react-query'; -import { apiGet } from '@/lib/api-client'; -import { queryKeys } from '@/lib/api/query-keys'; +import { queryKeys } from "@/lib/api/query-keys" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { apiGet, apiPost } from "@/lib/api-client" +import { finalReviewedReportResponseSchema, ReviewedReportSnapshotResponse } from "@ba-helper/contracts" + +export function useFinalReviewedReport(analysisId: string, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: queryKeys.documents.finalReviewedReport(analysisId), + queryFn: () => + apiGet( + `/api/v1/impact-analyses/${analysisId}/final-reviewed-report`, + finalReviewedReportResponseSchema, + ), + enabled: options?.enabled, + retry: false, // Do not retry if the gate blocks it + }) +} + +export function useLatestReviewedReportSnapshot(analysisId: string | undefined) { + return useQuery({ + queryKey: queryKeys.documents.reviewedReportSnapshot(analysisId || ""), + queryFn: async () => { + if (!analysisId) throw new Error("Analysis ID is required") + const result = await apiGet(`/api/v1/impact-analyses/${analysisId}/reviewed-report-snapshot/latest`) + return result as ReviewedReportSnapshotResponse + }, + enabled: !!analysisId, + retry: false, // Don't retry if 404 (no snapshot yet) + }) +} + +export function useCreateReviewedReportSnapshot(analysisId: string) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async () => { + const result = await apiPost(`/api/v1/impact-analyses/${analysisId}/reviewed-report-snapshot`, {}) + return result as ReviewedReportSnapshotResponse + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.documents.reviewedReportSnapshot(analysisId) + }) + }, + }) +} diff --git a/apps/web/src/hooks/api/use-impact-matrix.ts b/apps/web/src/hooks/api/use-impact-matrix.ts index c149b7e1..cf16c9d6 100644 --- a/apps/web/src/hooks/api/use-impact-matrix.ts +++ b/apps/web/src/hooks/api/use-impact-matrix.ts @@ -19,7 +19,7 @@ export function useMultiRepoImpactMatrix(runId: string) { export function useMatrixRowDetail(runId: string, analysisId: string | null) { return useQuery({ - queryKey: queryKeys.analyses.runs.impactMatrixRowDetail(runId, analysisId), + queryKey: queryKeys.analyses.runs.impactMatrixRowDetail(runId, analysisId || ""), queryFn: async () => { if (!analysisId) throw new Error("analysisId is required") const { matrixRowDetailResponseSchema } = await import("@ba-helper/contracts")