An enterprise AI Workspace, built on the Clean Architecture / CQRS / React stack defined
in .specify/memory/constitution.md and
docs/. Migrated from a legacy .NET 7 ASP.NET Core MVC application
("ChatGPT Client") — see
specs/000-legacy-modernization/spec.md for
the migration history; the legacy project has been decommissioned.
Ask Lucy.sln
src/
├── AskLucy.Domain/ # entities, value objects — no external dependencies
├── AskLucy.Application/ # CQRS commands/queries/handlers, interfaces (MediatR, FluentValidation)
├── AskLucy.Infrastructure/ # IAIProvider→OpenAIProvider, JWT, file storage, email
├── AskLucy.Persistence/ # EF Core DbContext, ASP.NET Identity, repositories
└── AskLucy.Web/ # controllers, JWT auth, rate limiting, Problem Details, OpenAPI
└── ClientApp/ # React 19 + TypeScript + Vite + Material UI — built into
# wwwroot on every build (see AskLucy.Web.csproj); served
# by the same process as the API
tests/
├── AskLucy.Domain.Tests/
├── AskLucy.Application.Tests/
├── AskLucy.Infrastructure.Tests/
├── AskLucy.Persistence.Tests/
├── AskLucy.Web.Tests/
└── AskLucy.E2E.Tests/ # Playwright — requires a live deployment, see its own header comment
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "<your SQL Server connection string>" --project src/AskLucy.Web
dotnet user-secrets set "Jwt:SigningKey" "<a random 32+ character string>" --project src/AskLucy.Web
dotnet user-secrets set "OpenAI:ApiKey" "<your OpenAI API key>" --project src/AskLucy.Web
dotnet user-secrets set "Smtp:Username" "<your SMTP username, if the host requires auth>" --project src/AskLucy.Web
dotnet user-secrets set "Smtp:Password" "<your SMTP password, if the host requires auth>" --project src/AskLucy.Web
dotnet user-secrets set "Geocoding:GoogleMapsApiKey" "<a server-side Google Maps key>" --project src/AskLucy.Web
cd src/AskLucy.Web/ClientApp && npm install && cd ../../.. # once, before the first build/run
dotnet build "Ask Lucy.sln"
dotnet run --project src/AskLucy.Web
AskLucy.Web.csproj runs npm run build and copies ClientApp's output into wwwroot
before every build (including hitting Run in Visual Studio) — one process serves both the
API and the SPA. This needs npm install to have been run in ClientApp at least once;
it does not run install/ci itself, to keep every build fast.
In the Development environment, email is never actually sent — ConsoleEmailSender logs it
instead, so the Smtp:* secrets above are only needed once you run outside Development.
Geocoding:GoogleMapsApiKey is optional but strongly recommended. Without it the app falls
back to NominatimGeocodingProvider, whose candidate importance is a Wikipedia-popularity
score rather than a match-quality one — an ordinary local place scores ~0.08 where Google
reports 0.40-0.90, so LocationResolution:MinimumImportanceFloor behaves quite differently on
the two. Use a server-side key (unrestricted or IP-restricted); the domain-restricted
browser key in ClientApp/.env (VITE_GOOGLE_MAPS_API_KEY) is rejected by server-to-server
calls. Which provider is live is logged once at startup:
Geocoding provider: GoogleMaps (Geocoding:GoogleMapsApiKey configured).
Never put real secrets in appsettings.json — see the _comment_secrets note in
src/AskLucy.Web/appsettings.json and constitution §8.
The build above always reflects ClientApp as of its last npm run build — for hot-reload
during active frontend work, run the Vite dev server separately instead:
cd src/AskLucy.Web/ClientApp
npm install
npm run dev
dotnet test # all backend test projects
cd src/AskLucy.Web/ClientApp && npm run test # frontend unit tests
Tesseract OCR language data (SPEC-015 Document Intelligence Pipeline, TesseractOcrEngine):
the Tesseract NuGet package brings its own native Windows engine binaries, but it does
not ship any .traineddata language files, and App_Data/ (where TesseractOcr:DataPath
in appsettings.json points, App_Data/tessdata) is gitignored — it is never populated by
a fresh clone, a CI build, or a deploy. On any new deployment host/container image, the
needed .traineddata files (at minimum eng.traineddata; add others from the
tessdata repo to match the languages your users
upload) must be placed under App_Data/tessdata manually before OCR will produce any text.
Missing a language pack is not fatal — TesseractOcrEngine falls back to whichever trained
data is actually present (defaulting to eng) rather than failing the pipeline — but a
missing eng.traineddata means OCR silently returns no text for every document.
Supertonic 3 voice model (specs/070, SupertonicTextToSpeechEngine): Lucy's on-server
voice needs the fp32 ONNX model and voice styles under App_Data/Models/supertonic-3
(Supertonic:ModelDirectory). Like tessdata, it is gitignored and never deployed by a build —
run scripts/download-supertonic.ps1 (pinned revision, SHA-256 verified) and copy the resulting
onnx/ and voice_styles/ folders to the host. The loaded model adds roughly 450 MB of resident
memory (weights are 398 MB), so the host plan must allow it. A missing model is not fatal: the voice
router fails over to the next provider (ElevenLabs) and logs why. Before making Supertonic
Lucy's voice in production, the OpenRAIL-M use restrictions in
docs/THIRD_PARTY_NOTICES.md must be passed through to users.
.specify/memory/constitution.md — the project's
engineering constitution; supersedes all other guidance.docs/ — target architecture, database, API, security, testing, and design
system standards.docs/adr/ — Architecture Decision Records.specs/ — feature specifications, plans, and tasks (Spec Kit workflow).CONTRIBUTING.md — branch protection and required CI secrets.C#
59.8%
TypeScript
27.6%
HTML
7.6%
Python
3.8%
An enterprise AI Workspace, built on the Clean Architecture / CQRS / React stack defined
in .specify/memory/constitution.md and
docs/. Migrated from a legacy .NET 7 ASP.NET Core MVC application
("ChatGPT Client") — see
specs/000-legacy-modernization/spec.md for
the migration history; the legacy project has been decommissioned.
Ask Lucy.sln
src/
├── AskLucy.Domain/ # entities, value objects — no external dependencies
├── AskLucy.Application/ # CQRS commands/queries/handlers, interfaces (MediatR, FluentValidation)
├── AskLucy.Infrastructure/ # IAIProvider→OpenAIProvider, JWT, file storage, email
├── AskLucy.Persistence/ # EF Core DbContext, ASP.NET Identity, repositories
└── AskLucy.Web/ # controllers, JWT auth, rate limiting, Problem Details, OpenAPI
└── ClientApp/ # React 19 + TypeScript + Vite + Material UI — built into
# wwwroot on every build (see AskLucy.Web.csproj); served
# by the same process as the API
tests/
├── AskLucy.Domain.Tests/
├── AskLucy.Application.Tests/
├── AskLucy.Infrastructure.Tests/
├── AskLucy.Persistence.Tests/
├── AskLucy.Web.Tests/
└── AskLucy.E2E.Tests/ # Playwright — requires a live deployment, see its own header comment
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "<your SQL Server connection string>" --project src/AskLucy.Web
dotnet user-secrets set "Jwt:SigningKey" "<a random 32+ character string>" --project src/AskLucy.Web
dotnet user-secrets set "OpenAI:ApiKey" "<your OpenAI API key>" --project src/AskLucy.Web
dotnet user-secrets set "Smtp:Username" "<your SMTP username, if the host requires auth>" --project src/AskLucy.Web
dotnet user-secrets set "Smtp:Password" "<your SMTP password, if the host requires auth>" --project src/AskLucy.Web
dotnet user-secrets set "Geocoding:GoogleMapsApiKey" "<a server-side Google Maps key>" --project src/AskLucy.Web
cd src/AskLucy.Web/ClientApp && npm install && cd ../../.. # once, before the first build/run
dotnet build "Ask Lucy.sln"
dotnet run --project src/AskLucy.Web
AskLucy.Web.csproj runs npm run build and copies ClientApp's output into wwwroot
before every build (including hitting Run in Visual Studio) — one process serves both the
API and the SPA. This needs npm install to have been run in ClientApp at least once;
it does not run install/ci itself, to keep every build fast.
In the Development environment, email is never actually sent — ConsoleEmailSender logs it
instead, so the Smtp:* secrets above are only needed once you run outside Development.
Geocoding:GoogleMapsApiKey is optional but strongly recommended. Without it the app falls
back to NominatimGeocodingProvider, whose candidate importance is a Wikipedia-popularity
score rather than a match-quality one — an ordinary local place scores ~0.08 where Google
reports 0.40-0.90, so LocationResolution:MinimumImportanceFloor behaves quite differently on
the two. Use a server-side key (unrestricted or IP-restricted); the domain-restricted
browser key in ClientApp/.env (VITE_GOOGLE_MAPS_API_KEY) is rejected by server-to-server
calls. Which provider is live is logged once at startup:
Geocoding provider: GoogleMaps (Geocoding:GoogleMapsApiKey configured).
Never put real secrets in appsettings.json — see the _comment_secrets note in
src/AskLucy.Web/appsettings.json and constitution §8.
The build above always reflects ClientApp as of its last npm run build — for hot-reload
during active frontend work, run the Vite dev server separately instead:
cd src/AskLucy.Web/ClientApp
npm install
npm run dev
dotnet test # all backend test projects
cd src/AskLucy.Web/ClientApp && npm run test # frontend unit tests
Tesseract OCR language data (SPEC-015 Document Intelligence Pipeline, TesseractOcrEngine):
the Tesseract NuGet package brings its own native Windows engine binaries, but it does
not ship any .traineddata language files, and App_Data/ (where TesseractOcr:DataPath
in appsettings.json points, App_Data/tessdata) is gitignored — it is never populated by
a fresh clone, a CI build, or a deploy. On any new deployment host/container image, the
needed .traineddata files (at minimum eng.traineddata; add others from the
tessdata repo to match the languages your users
upload) must be placed under App_Data/tessdata manually before OCR will produce any text.
Missing a language pack is not fatal — TesseractOcrEngine falls back to whichever trained
data is actually present (defaulting to eng) rather than failing the pipeline — but a
missing eng.traineddata means OCR silently returns no text for every document.
Supertonic 3 voice model (specs/070, SupertonicTextToSpeechEngine): Lucy's on-server
voice needs the fp32 ONNX model and voice styles under App_Data/Models/supertonic-3
(Supertonic:ModelDirectory). Like tessdata, it is gitignored and never deployed by a build —
run scripts/download-supertonic.ps1 (pinned revision, SHA-256 verified) and copy the resulting
onnx/ and voice_styles/ folders to the host. The loaded model adds roughly 450 MB of resident
memory (weights are 398 MB), so the host plan must allow it. A missing model is not fatal: the voice
router fails over to the next provider (ElevenLabs) and logs why. Before making Supertonic
Lucy's voice in production, the OpenRAIL-M use restrictions in
docs/THIRD_PARTY_NOTICES.md must be passed through to users.
.specify/memory/constitution.md — the project's
engineering constitution; supersedes all other guidance.docs/ — target architecture, database, API, security, testing, and design
system standards.docs/adr/ — Architecture Decision Records.specs/ — feature specifications, plans, and tasks (Spec Kit workflow).CONTRIBUTING.md — branch protection and required CI secrets.C#
59.8%
TypeScript
27.6%
HTML
7.6%
Python
3.8%