Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a28396b
docs: remove outdated audits and superseded glossary/workflow docs
hungthinh1104 Jun 24, 2026
c22ce3d
docs: avoid overclaim wording in proof materials
hungthinh1104 Jun 24, 2026
4773adc
docs: remove more outdated agent docs (product-vision, technical-debt…
hungthinh1104 Jun 24, 2026
d9414a8
feat(contracts): define analysis workspace presentation model
hungthinh1104 Jun 24, 2026
2515061
feat(api): add analysis workspace read model
hungthinh1104 Jun 24, 2026
5856511
refactor(web): render analysis workspace from read model
hungthinh1104 Jun 24, 2026
796d679
feat(web): add analysis localization foundation
hungthinh1104 Jun 24, 2026
f6a7acc
docs: define security scale and performance hardening model
hungthinh1104 Jun 24, 2026
14fb4ed
fix(analyzer): skip binary content during safe file enumeration
hungthinh1104 Jun 24, 2026
ba8c6ba
feat(contracts): define analysis lineage diff model
hungthinh1104 Jun 24, 2026
f3fbfa2
feat(api): expose analysis lineage diff read model
hungthinh1104 Jun 24, 2026
81f59d0
feat(web): add analysis lineage diff workspace
hungthinh1104 Jun 24, 2026
b51b58b
feat(domain-packs): add domain profile registry
hungthinh1104 Jun 24, 2026
3a870b1
feat(document): support locale-aware report rendering
hungthinh1104 Jun 25, 2026
54e7a2f
test(domain-packs): add booking evaluation cases
hungthinh1104 Jun 25, 2026
be35b7a
test(domain-packs): harden general fallback evaluation
hungthinh1104 Jun 25, 2026
64e43f1
style(web): align analysis workspace visual tokens
hungthinh1104 Jun 25, 2026
1d0a9b7
style(web): namespace landing and workspace css classes
hungthinh1104 Jun 25, 2026
dc119a7
fix(web): pass localized finalization labels
hungthinh1104 Jun 25, 2026
788ce83
feat(domain-packs): add rental partial domain profile
hungthinh1104 Jun 25, 2026
7379f6a
feat(web,api): p8a ux polish and markdown readability
hungthinh1104 Jun 25, 2026
bc884f4
chore(config): consolidate local environment loading
hungthinh1104 Jun 25, 2026
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
105 changes: 101 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,103 @@
DATABASE_URL=postgresql://USER:PASSWORD@localhost:5432/ba_helper
# ==========================================
# Database & Queue
# ==========================================
DATABASE_URL=postgresql://ba_helper:ba_helper@localhost:5432/ba_helper
REDIS_URL=redis://localhost:6379

# Root env is intentionally minimal. App-specific runtime examples live in:
# - apps/api/.env.example
# - apps/web/.env.example
# ==========================================
# Backend App (API & Worker)
# ==========================================
# SECURITY HYGIENE:
# - In Development (NODE_ENV=development), missing or default environment variables may fallback to safe local values (e.g., localhost DB, 'dev-secret').
# - In Production (NODE_ENV=production), the application is designed to FAIL FAST if critical variables (e.g., JWT_SECRET, DATABASE_URL) are missing or set to weak defaults.

PORT=3001
NODE_ENV=development
WORKSPACE_MODE=dev-single-user
ENABLE_DEV_LOGIN=true
JWT_SECRET=replace-with-a-long-random-secret-32-chars-min
CORS_ALLOWED_ORIGINS=http://localhost:3000
PUBLIC_PREVIEW_MODE=false

# ==========================================
# Web App (Next.js)
# ==========================================
NEXT_PUBLIC_API_URL=http://localhost:3001
INTERNAL_API_URL=http://localhost:3001
NEXTAUTH_SECRET=replace-with-a-long-random-secret-32-chars-min

# Set to true to use mock data for UI development without backend
# Note: This is strictly forbidden in production.
NEXT_PUBLIC_USE_MOCK_API=false

# ==========================================
# Public Preview Guard (Basic Auth)
# ==========================================
# Enables Basic Auth middleware to protect public preview deployments.
PREVIEW_AUTH_ENABLED=false
PREVIEW_USERNAME=demo
PREVIEW_PASSWORD=change-me

# ==========================================
# AI Provider (LLM)
# ==========================================
# Options: fake | google | openai | anthropic | deepseek
# Demo runtime default: google/Gemini for real LLM output.
# Automated tests override this to fake so CI never calls external APIs.
AI_PROVIDER=google

# Temperature and token limits (apply to all providers unless overridden)
AI_TEMPERATURE=0.2
AI_MAX_TOKENS=8192

# ==========================================
# Google Gemini (AI_PROVIDER=google)
# ==========================================
# Key lookup priority: GOOGLE_API_KEY > GEMINI_API_KEY > GOOGLE_AI_API_KEY
GOOGLE_API_KEY=
GEMINI_API_KEY=
GOOGLE_MODEL=gemini-2.5-flash

# ==========================================
# OpenAI (AI_PROVIDER=openai)
# ==========================================
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o

# ==========================================
# Anthropic (AI_PROVIDER=anthropic)
# ==========================================
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-3-5-sonnet-20241022

# ==========================================
# DeepSeek (AI_PROVIDER=deepseek)
# ==========================================
DEEPSEEK_API_KEY=
DEEPSEEK_MODEL=deepseek-chat
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_TEMPERATURE=0
DEEPSEEK_MAX_TOKENS=4096

# ==========================================
# Embedding Provider
# ==========================================
# Options: fake | openai | google
# Demo keeps embeddings local/fake unless you intentionally validate real vector retrieval.
EMBEDDING_PROVIDER=fake
GOOGLE_EMBEDDING_MODEL=gemini-embedding-001
OPENAI_EMBEDDING_MODEL=text-embedding-3-small

# ==========================================
# Smoke / E2E
# ==========================================
# Set REAL_PATH_SMOKE=true to run with real vector retrieval + LLM.
# Set GEMINI_API_KEY or GOOGLE_API_KEY before running real LLM smoke.
REAL_PATH_SMOKE=false
SMOKE_ALLOW_DEV_LOGIN_FALLBACK=false
SMOKE_DEV_LOGIN_EMAIL=smoke-admin@ba-helper.local
SMOKE_DEV_LOGIN_ROLE=ADMIN
RUN_REAL_LLM_BENCHMARK=false
SMOKE_API_URL=http://localhost:3001
SMOKE_POLL_INTERVAL_MS=2000
SMOKE_REQUEST_TIMEOUT_MS=15000
88 changes: 3 additions & 85 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,86 +1,4 @@
# BA Helper API — Environment Variables Reference
# ENVIRONMENT VARIABLES CONSOLIDATED
#
# SECURITY HYGIENE:
# - In Development (NODE_ENV=development), missing or default environment variables may fallback to safe local values (e.g., localhost DB, 'dev-secret').
# - In Production (NODE_ENV=production), the application is designed to FAIL FAST if critical variables (e.g., JWT_SECRET, DATABASE_URL) are missing or set to weak defaults.

# ==========================================
# Database & Queue
# ==========================================
DATABASE_URL=postgresql://ba_helper:ba_helper@localhost:5432/ba_helper
REDIS_URL=redis://localhost:6379
WORKSPACE_MODE=dev-single-user
ENABLE_DEV_LOGIN=true
JWT_SECRET=replace-with-a-long-random-secret-32-chars-min
CORS_ALLOWED_ORIGINS=http://localhost:3000
PUBLIC_PREVIEW_MODE=false

# ==========================================
# AI Provider (LLM)
# ==========================================
# Options: fake | google | openai | anthropic | deepseek
# Demo runtime default: google/Gemini for real LLM output.
# Automated tests override this to fake so CI never calls external APIs.
AI_PROVIDER=google

# Temperature and token limits (apply to all providers unless overridden)
AI_TEMPERATURE=0.2
AI_MAX_TOKENS=8192

# ==========================================
# Google Gemini (AI_PROVIDER=google)
# ==========================================
# Key lookup priority: GOOGLE_API_KEY > GEMINI_API_KEY > GOOGLE_AI_API_KEY
GOOGLE_API_KEY=
GEMINI_API_KEY=
GOOGLE_MODEL=gemini-2.5-flash

# ==========================================
# OpenAI (AI_PROVIDER=openai)
# ==========================================
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o

# ==========================================
# Anthropic (AI_PROVIDER=anthropic)
# ==========================================
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-3-5-sonnet-20241022

# ==========================================
# DeepSeek (AI_PROVIDER=deepseek)
# ==========================================
DEEPSEEK_API_KEY=
DEEPSEEK_MODEL=deepseek-chat
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_TEMPERATURE=0
DEEPSEEK_MAX_TOKENS=4096

# ==========================================
# Embedding Provider
# ==========================================
# Options: fake | openai | google
# Demo keeps embeddings local/fake unless you intentionally validate real vector retrieval.
EMBEDDING_PROVIDER=fake
GOOGLE_EMBEDDING_MODEL=gemini-embedding-001
OPENAI_EMBEDDING_MODEL=text-embedding-3-small

# ==========================================
# Smoke / E2E
# ==========================================
# Set REAL_PATH_SMOKE=true to run with real vector retrieval + LLM.
# Set GEMINI_API_KEY or GOOGLE_API_KEY before running real LLM smoke.
REAL_PATH_SMOKE=false
SMOKE_ALLOW_DEV_LOGIN_FALLBACK=false
SMOKE_DEV_LOGIN_EMAIL=smoke-admin@ba-helper.local
SMOKE_DEV_LOGIN_ROLE=ADMIN
RUN_REAL_LLM_BENCHMARK=false
SMOKE_API_URL=http://localhost:3001
SMOKE_POLL_INTERVAL_MS=2000
SMOKE_REQUEST_TIMEOUT_MS=15000

# ==========================================
# App
# ==========================================
PORT=3001
NODE_ENV=development
# All environment variables have been consolidated to the project root.
# Please use the `/.env.example` file at the root of the repository to configure this application.
14 changes: 7 additions & 7 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
"main": "dist/main.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "pnpm exec ts-node -r tsconfig-paths/register --project tsconfig.json src/main.ts",
"dev": "dotenv -e ../../.env -- pnpm exec ts-node -r tsconfig-paths/register --project tsconfig.json src/main.ts",
"lint": "echo \"lint api\"",
"smoke:public-github": "tsx src/smoke-e2e.ts",
"smoke:public-github:real-llm": "REAL_LLM_SMOKE=true tsx src/smoke-e2e.ts",
"smoke:public-github:real-path": "REAL_PATH_SMOKE=true tsx src/smoke-e2e.ts",
"smoke:public-github": "dotenv -e ../../.env -- tsx src/smoke-e2e.ts",
"smoke:public-github:real-llm": "REAL_LLM_SMOKE=true dotenv -e ../../.env -- tsx src/smoke-e2e.ts",
"smoke:public-github:real-path": "REAL_PATH_SMOKE=true dotenv -e ../../.env -- tsx src/smoke-e2e.ts",
"test": "jest",
"typecheck": "tsc --noEmit",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio"
"prisma:generate": "dotenv -e ../../.env -- prisma generate",
"prisma:migrate": "dotenv -e ../../.env -- prisma migrate dev",
"prisma:studio": "dotenv -e ../../.env -- prisma studio"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.99.0",
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'reflect-metadata';
import 'dotenv/config';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { apiReference } from '@scalar/nestjs-api-reference';
Expand Down
9 changes: 7 additions & 2 deletions apps/api/src/modules/document/api/document.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Param, Post, Body, Res } from '@nestjs/common';
import { Controller, Get, Param, Post, Body, Query, Res } from '@nestjs/common';
import { documentListResponseSchema } from '@ba-helper/contracts';
import { ListDocumentsUseCase } from '../application/queries/list-documents.usecase';
import { GetApprovedReportUseCase } from '../application/get-approved-report.usecase';
Expand All @@ -12,6 +12,7 @@ import {
reviewedReportSnapshotSchema,
finalReviewedReportResponseSchema,
documentJobSchema,
localeAwareReportQuerySchema,
RequestUser
} from '@ba-helper/contracts';
import { DocumentMapper } from './document.mapper';
Expand Down Expand Up @@ -153,11 +154,15 @@ export class DocumentController {
@Get('/impact-analyses/:analysisId/final-reviewed-report')
async getFinalReviewedReportGate(
@Param('analysisId') analysisId: string,
@Query() query: unknown,
@CurrentUser() actor: RequestUser,
) {
await this.permissions.assertCanReadAnalysis(actor, analysisId);
const parsedQuery = localeAwareReportQuerySchema.parse(query ?? {});

const result = await this.getFinalReviewedReport.execute(analysisId);
const result = await this.getFinalReviewedReport.execute(analysisId, {
locale: parsedQuery.locale,
});

return finalReviewedReportResponseSchema.parse(result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class RunDocumentJobUseCase {
const analysis = await this.prisma.impactAnalysis.findUnique({
where: { id: snapshot.analysisId },
include: {
snapshot: { include: { repository: true } },
snapshot: { include: { repository: true, profile: true } },
sourceTarget: true,
requirementRevision: { include: { requirement: true } },
insights: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Prisma, ReviewNote } from '@prisma/client';
import { ClarificationItemDto } from '@ba-helper/contracts';
import { ApprovedReportMetadata } from '../domain/approved-report-metadata';
import { ReportDependencyEdge } from './mermaid-impact-diagram.builder';
import { ReportLocale } from './render/report-localization';

export type AnalysisSnapshot = Prisma.ImpactAnalysisGetPayload<{
include: {
snapshot: { include: { repository: true } };
snapshot: { include: { repository: true; profile: true } };
sourceTarget: true;
requirementRevision: true;
};
Expand Down Expand Up @@ -34,6 +35,7 @@ export type TraceabilityLinkWithArtifact = Prisma.TraceabilityLinkGetPayload<{

export type MarkdownReportRenderContext = {
analysis: AnalysisSnapshot;
locale: ReportLocale;
insights: InsightWithEvidence[];
traceabilityLinks: TraceabilityLinkWithArtifact[];
reviewNotes: ReviewNote[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ describe('GetFinalReviewedReportUseCase', () => {
let getReviewCompletionMock: any;
let getLatestSnapshotMock: any;
let prismaMock: any;
let contextAdapterMock: any;
let reportBuilderMock: any;

beforeEach(() => {
getReviewCompletionMock = {
Expand All @@ -21,12 +23,23 @@ describe('GetFinalReviewedReportUseCase', () => {
documentJob: {
findFirst: jest.fn(),
},
impactAnalysis: {
findUnique: jest.fn(),
},
};
contextAdapterMock = {
buildContext: jest.fn(),
};
reportBuilderMock = {
build: jest.fn(),
};

useCase = new GetFinalReviewedReportUseCase(
getReviewCompletionMock as any,
getLatestSnapshotMock as any,
prismaMock as any,
contextAdapterMock as any,
reportBuilderMock as any,
);
});

Expand Down Expand Up @@ -88,6 +101,7 @@ describe('GetFinalReviewedReportUseCase', () => {
expect(result).toEqual({
analysisId: 'analysis-123',
snapshotId: 'snap-1',
locale: 'en',
markdown: '# Generated Document Markdown',
createdAt: '2026-01-01T00:00:00.000Z',
reviewCompletion: mockCompletion,
Expand Down Expand Up @@ -122,6 +136,45 @@ describe('GetFinalReviewedReportUseCase', () => {
const result = await useCase.execute('analysis-123');

expect(result.markdown).toBe('# Job Document');
expect(result.locale).toBe('en');
});

it('renders localized markdown from the reviewed snapshot after default document readiness is satisfied', async () => {
const mockCompletion = {
isComplete: true,
blockingReasons: [],
};
const mockSnapshot = {
id: 'snap-1',
analysisId: 'analysis-123',
approvedDocumentId: 'doc-1',
markdown: null,
createdAt: new Date('2026-01-01T00:00:00.000Z'),
reviewDecisionsSnapshot: [],
evidenceQualitySummarySnapshot: {},
evaluationContextSnapshot: null,
createdByUserId: null,
};
const mockAnalysis = { id: 'analysis-123' };
const mockContext = { locale: 'vi' };

getReviewCompletionMock.execute.mockResolvedValue(mockCompletion);
getLatestSnapshotMock.execute.mockResolvedValue(mockSnapshot);
prismaMock.generatedDocument.findUnique.mockResolvedValue({
id: 'doc-1',
content: '# English persisted document',
});
prismaMock.impactAnalysis.findUnique.mockResolvedValue(mockAnalysis);
contextAdapterMock.buildContext.mockResolvedValue(mockContext);
reportBuilderMock.build.mockReturnValue('# Bao cao tieng Viet\n```ts\nconsole.log("raw evidence");\n```');

const result = await useCase.execute('analysis-123', { locale: 'vi' });

expect(result.locale).toBe('vi');
expect(result.markdown).toContain('# Bao cao tieng Viet');
expect(result.markdown).toContain('console.log("raw evidence");');
expect(contextAdapterMock.buildContext).toHaveBeenCalledWith(mockSnapshot, mockAnalysis, 'vi');
expect(reportBuilderMock.build).toHaveBeenCalledWith(mockContext);
});

it('throws a document readiness error when the async document job is still queued', async () => {
Expand Down
Loading
Loading