Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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."
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
10 changes: 5 additions & 5 deletions apps/api/src/modules/document/api/document.controller.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Injectable } from '@nestjs/common';
import { DocumentJobStatus } from '@prisma/client';
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 {
constructor(
private readonly prisma: PrismaService,
private readonly reportBuilder: MarkdownImpactReportBuilder,
private readonly documentRepo: DocumentRepository,
private readonly contextAdapter: ReviewedSnapshotReportContextAdapter,
) {}

async execute(params: { documentJobId: string }) {
const docJob = await this.markRunning(params.documentJobId);

try {
const snapshot = await this.prisma.reviewedReportSnapshot.findUnique({
where: { id: docJob.snapshotId },
});
if (!snapshot) {
throw new AppError('SNAPSHOT_NOT_FOUND', 'Reviewed report snapshot not found.');
}

const analysis = await this.prisma.impactAnalysis.findUnique({
where: { id: snapshot.analysisId },
include: {
snapshot: { include: { repository: true } },
sourceTarget: true,
requirementRevision: { include: { requirement: true } },
insights: true,
},
});
if (!analysis) {
throw new AppError('IMPACT_ANALYSIS_NOT_FOUND', 'Impact analysis not found.');
}

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,
});

await this.prisma.$transaction(async (tx) => {
await tx.documentJob.update({
where: { id: docJob.id },
data: {
status: DocumentJobStatus.COMPLETED,
progress: 100,
completedAt: new Date(),
generatedDocumentId: persistedReport.id,
},
});

await tx.reviewedReportSnapshot.update({
where: { id: snapshot.id },
data: { approvedDocumentId: persistedReport.id },
});
});

return { success: true, generatedDocumentId: persistedReport.id };
} catch (error) {
await this.prisma.documentJob.update({
where: { id: docJob.id },
data: {
status: DocumentJobStatus.FAILED,
error: this.toErrorJson(error),
failedAt: new Date(),
},
});
throw error;
}
}

private async markRunning(documentJobId: string) {
const job = await this.prisma.documentJob.findUnique({
where: { id: documentJobId },
});

if (!job) {
throw new AppError('DOCUMENT_JOB_NOT_FOUND', 'Document job not found.');
}

if (job.status !== DocumentJobStatus.QUEUED && job.status !== DocumentJobStatus.RUNNING) {
throw new AppError('DOCUMENT_JOB_NOT_READY', 'Document job is not queued or running.', {
status: job.status,
});
}

return this.prisma.documentJob.update({
where: { id: documentJobId },
data: {
status: DocumentJobStatus.RUNNING,
lastStartedAt: new Date(),
},
});
}



private toErrorJson(error: unknown) {
if (error instanceof Error) {
return {
message: error.message,
name: error.name,
};
}
return {
message: String(error),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export type MarkdownReportRenderContext = {
dependencyEdges: ReportDependencyEdge[];
clarifications: ClarificationItemDto[];
reviewDecisions: any[];
reviewDecisionsSnapshot?: any[];
evidenceQualitySummarySnapshot?: any;
diff?: any;
metadata?: ApprovedReportMetadata;
};
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DocumentRepository } from '../infrastructure/document.repository';
import { DocumentRepository } from '../../infrastructure/document.repository';

export class ListDocumentsUseCase {
constructor(private readonly repository: DocumentRepository) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EvaluationContextAdapter } from '../evaluation-context.adapter';
import { EvaluationContextAdapter } from '../../evaluation-context.adapter';

export function renderEvaluationContext(evalContext: ReturnType<EvaluationContextAdapter['getEvaluationContext']>): string[] {
const lines: string[] = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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('');

Expand Down
Loading
Loading