From e14c4e14b1a156196daaa0059f173b0bf63be315 Mon Sep 17 00:00:00 2001 From: Michal Wcislo Date: Sat, 31 Oct 2026 12:13:33 +0100 Subject: [PATCH] chore: Refactors Claude files --- CLAUDE.md | 7 +-- docs/adr/README.md | 24 ++++----- docs/architecture.md | 107 +------------------------------------- docs/diagrams.md | 3 +- docs/repository-layout.md | 90 ++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 122 deletions(-) create mode 100644 docs/repository-layout.md diff --git a/CLAUDE.md b/CLAUDE.md index 0c60c97..002cd5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,16 +6,17 @@ It is a simplified model of a real-world integration. Not production code, but i under failure: duplicates, retries, crashes, outages. - Business context, glossary, domain rules D1–D6: @docs/domain.md -- Containers, repository layout, dependency rules, key flow, limitations: @docs/architecture.md +- Containers, dependency rules, key flow, limitations: @docs/architecture.md ## Where things are | Document | Kind | Loaded | |---|---|---| | `docs/domain.md` | Normative: business language and domain rules | Always (imported above) | -| `docs/architecture.md` | Dependency rules are normative; layout and flows are descriptive | Always (imported above) | +| `docs/architecture.md` | Dependency rules are normative; flows are descriptive | Always (imported above) | +| `docs/repository-layout.md` | Descriptive: every project and folder, what it holds, what it references | Open it before adding, moving or removing a project, folder or tool. Keep it true in the same change | | `docs/acceptance-scenarios.md` | Normative: expected behaviour, one integration test per scenario | Open it before writing or changing tests or behaviour | -| `docs/adr/` | Normative: decisions and rationale. Index: `docs/adr/README.md` | Open the relevant ADR before changing what it decides. Summary of all ADRs: the ADR table in `architecture.md` | +| `docs/adr/` | Normative: decisions and rationale. Index: `docs/adr/README.md` | Open the relevant ADR before changing what it decides. One-line summary of each: the table in `docs/adr/README.md` | | `.claude/rules/invariants.md` | Normative | Always | | Other `.claude/rules/*.md` | Rules for specific paths (`paths:` frontmatter) | When a matching file is read or written | | `simulators/CLAUDE.md` | Rules for the fake external systems | When a file in `simulators/` is read or written | diff --git a/docs/adr/README.md b/docs/adr/README.md index b50342b..d6eec3e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -3,16 +3,16 @@ Normative: decisions and rationale. Format: Michael Nygard (Title, Status, Context, Decision, Consequences), plus Alternatives considered. -| ADR | Title | Status | +| ADR | Decision | Status | |---|---|---| -| [001](001-project-structure.md) | Project structure | Accepted | -| [002](002-event-hubs-over-service-bus.md) | Event Hubs over Service Bus | Accepted | -| [003](003-notify-then-fetch.md) | Notify-then-fetch | Accepted | -| [004](004-transactional-outbox.md) | Transactional outbox in Ingress | Accepted | -| [005](005-idempotency.md) | Idempotency | Accepted | -| [006](006-partition-key-candidate-id.md) | Partition key = candidateId | Accepted | -| [007](007-llm-enrichment.md) | LLM enrichment | Accepted | -| [008](008-source-of-truth-and-sync-direction.md) | Source of truth and sync direction | Accepted | -| [009](009-prompt-evaluation.md) | Offline evaluation of the enrichment prompt | Accepted | -| [010](010-observability.md) | Observability | Accepted | -| [011](011-kubernetes-deployment.md) | Kubernetes deployment | Accepted | +| [001](001-project-structure.md) | Project structure: by context first, then by layer; a project wherever the compiler should enforce a rule; no Common project | Accepted | +| [002](002-event-hubs-over-service-bus.md) | Event Hubs over Service Bus: replay, independent consumers; cost: no DLQ → parking lot and failure classification | Accepted | +| [003](003-notify-then-fetch.md) | Notify-then-fetch: event is a signal, API state is the truth (ordering, partial payloads, unclear redelivery) | Accepted | +| [004](004-transactional-outbox.md) | Transactional outbox in Ingress: persist before 200, a single relay publishes; cost: 503 when SQL is down loses the event | Accepted | +| [005](005-idempotency.md) | Idempotency: idempotent effects, per-worker inbox, checkpoint after processing, stored LLM results | Accepted | +| [006](006-partition-key-candidate-id.md) | Partition key = `candidateId`, consequences for scaling and the job application payload assumption | Accepted | +| [007](007-llm-enrichment.md) | LLM enrichment: own port over `IChatClient` (Claude via the Anthropic SDK), PII masking, validation, short retries in the provider SDK inside our timeout, fallback to `NotEnriched`, enrichment inline in the handler | Accepted | +| [008](008-source-of-truth-and-sync-direction.md) | Source of truth and sync direction: one way from Teamtailor, ownership per field, full replace of the fields we own (last write wins) | Accepted | +| [009](009-prompt-evaluation.md) | Offline evaluation of the enrichment prompt: golden set of fictional CVs, metrics in code, report per `(PromptVersion, GoldenSetHash)`, run manually never in CI; a CI test only checks that a report exists for the current pair | Accepted | +| [010](010-observability.md) | Observability: OpenTelemetry over OTLP, Aspire Dashboard locally, one `Infrastructure.Observability` project referenced by the hosts, trace context propagated explicitly through the outbox and `EventData` properties, one consumer span per attempt | Accepted | +| [011](011-kubernetes-deployment.md) | Kubernetes deployment: only our hosts in the cluster (kind locally), infrastructure in docker compose, one Helm chart, relay switched by `OutboxRelay:Enabled` and run as a one-replica StatefulSet, migrations as a Helm hook Job before any host rolls out | Accepted | diff --git a/docs/architecture.md b/docs/architecture.md index cc89ae9..23bdb2b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3,6 +3,7 @@ Business context, glossary and domain rules D1–D6: `docs/domain.md`. Expected behaviour and failure scenarios: `docs/acceptance-scenarios.md`. Mermaid diagrams of the containers and the key flow (for humans): `docs/diagrams.md`. +Repository layout (projects, folders, tools): `docs/repository-layout.md`. ADR summaries: `docs/adr/README.md`. The **dependency rules** below are normative. Layout, flows and names are descriptive: once code exists, the code is current (see "When docs and code disagree" in `CLAUDE.md`). @@ -22,94 +23,6 @@ Three deployables, split by runtime behaviour, not by business capability. They | Simulators | Stand-ins for Teamtailor, CRM, Matching Platform; break on demand | Production behaviour | in-memory | | Aspire Dashboard | Local view of the hosts' traces, metrics and logs over OTLP (ADR 010) | Run in tests or CI | in-memory | -## Repository layout - -``` -README.md for humans: overview, running locally, simplifications -TalentSync.slnx solution, folders mirror the directories below -global.json SDK pin, test runner = Microsoft.Testing.Platform -dotnet-tools.json local tool manifest: dotnet-ef (migrations, used by deploy/run-local.sh) -Directory.Build.props net10.0, nullable, implicit usings, warnings as errors -Directory.Packages.props central package versions (CPM) -src/ - Shared/ - TalentSync.Contracts event envelope, event names, SchemaVersion. References nothing - TalentSync.Recruitment.Domain our read-only model of recruitment: Candidate, JobApplication, - ApplicationStatus, FetchResult (Found | Deleted | Rejected), - port IRecruitmentSource. References nothing - TalentSync.Infrastructure.Teamtailor ACL: TeamtailorRecruitmentSource (port adapter) over TeamtailorApiClient - (HTTP, status mapping) and TeamtailorMapper, internal JSON:API DTOs, 429 handling, - stage mapping from config, 404 → Deleted, 4xx / unmappable → Rejected, - webhook parsing, signature v2 endpoint filter, - public AddTeamtailorWebhooks() (Ingress) - and AddTeamtailorApi() (workers). → Recruitment.Domain - TalentSync.Infrastructure.Observability AddObservability(): OpenTelemetry traces, metrics and logs over OTLP, - subscribes to every TalentSync.* ActivitySource and Meter, HTTP and SQL - instrumentation, SpanExportPolicy (ADR 010). - Only hosts reference it. References nothing in src/ - TalentSync.Infrastructure.Messaging EventProcessorClient hosting, consumer handler skeleton (retry loop, - failure classification), HandlerResult, Inbox and ParkingLot entities - with schema-parameterised EF config. → Contracts - Ingress/ - TalentSync.Ingress.Api host: webhook endpoint, envelope, IngressDbContext + Outbox, relay and - EventHubProducerClient (only when OutboxRelay:Enabled), /health/live. - → Contracts, Infrastructure.Teamtailor - Sync/ - TalentSync.Sync.Application UseCases/SyncCandidateToCrmUseCase, Ports/ICrm. → Recruitment.Domain - TalentSync.Sync.Infrastructure CrmClient + resilience, SyncDbContext. → Sync.Application, Infrastructure.Messaging - TalentSync.Sync.Worker host: candidate.* handler (use case result → HandlerResult), - composition root, group "sync-worker". Web host: only /health/live - (liveness for an orchestrator, no dependency checks) - Matching/ - TalentSync.Matching.Domain ConsultantProfile, CandidateId, Skill, Confidence, Seniority, - Enrichment (+ TryCreate → EnrichmentValidation), EnrichmentStatus. References nothing - TalentSync.Matching.Application UseCases/CreateConsultantProfileUseCase, Ports/IMatchingPlatform, - IEnrichmentService, IEnrichmentResultStore, CvHash. - → Matching.Domain, Recruitment.Domain - TalentSync.Matching.Infrastructure Llm/ (CvMasker, EnrichmentPrompt, ChatEnrichmentService over IChatClient, - Fake/FakeChatClient), - EnrichmentResults/, MatchingPlatformClient, MatchingDbContext with - EnrichmentResultStore. → Matching.Application, Infrastructure.Messaging - TalentSync.Matching.Worker host: job_application.* handler (use case result → HandlerResult), - composition root, group "matching-worker". Web host: only /health/live - (liveness for an orchestrator, no dependency checks) -simulators/ fake external systems, NOT part of the product (see simulators/CLAUDE.md). - FakeTeamtailor No references to src/. - FakeCrm - FakeMatchingPlatform -tests/ - TalentSync.Matching.Domain.Tests - TalentSync.Sync.Application.Tests use case: fetch result and CRM result → use case result - TalentSync.Matching.Application.Tests use case: qualified / not qualified / deleted / rejected, stored or new - enrichment (stored before the upsert), failure → NotEnriched - TalentSync.Matching.Infrastructure.Tests Matching Platform client: request body, status mapping, resilience - TalentSync.Infrastructure.Teamtailor.Tests webhook parsing, stage mapping, 404 → Deleted, 4xx → Rejected - TalentSync.Infrastructure.Messaging.Tests consumer skeleton: inbox / parking lot mapping, failure classification, handler loop - TalentSync.Infrastructure.Observability.Tests request span is a root with the SDK enabled, SpanExportPolicy - TalentSync.Sync.Infrastructure.Tests CRM client: status mapping, 404 on delete = done, resilience - TalentSync.Matching.Evaluation.Tests prompt evaluation: metrics, golden set hash, case files, report - TalentSync.IntegrationTests acceptance scenarios (see .claude/rules/testing.md) -eval/ offline evaluation of the enrichment prompt (ADR 009), never run in CI - README.md what each metric catches, how to read a report - TalentSync.Matching.Evaluation console runner over ChatEnrichmentService. → Matching.Infrastructure - cases/ golden set: one JSON per fictional CV - reports/ committed reports; local/ is gitignored - run-eval.sh runs the evaluation locally; provider, runs, output and model as variables at the top -deploy/ docker-compose.yml (infra + simulators + Aspire Dashboard), simulator.Dockerfile, - eventhubs/Config.json, run-local.sh (containers, migrations, the three hosts), - host.Dockerfile (one image per host), migrations.Dockerfile + migrate.sh - (idempotent EF scripts applied with sqlcmd, ADR 011) -docs/ domain.md, architecture.md, acceptance-scenarios.md, diagrams.md, - adr/ (index: adr/README.md), e2e.http (end-to-end demo by hand), - images/ (screenshots for the README) -.github/workflows/ci.yml on PRs and main: Build job, then Test job ("Test results" check, coverage summary) -.claude/rules/ topic rules, some load only for matching paths -.claude/settings.json permissions and hooks enforced by the harness -.claude/hooks/ check-rules.sh: Teamtailor in Domain/Application, direct clock in src/ -.gitattributes *.sh keeps LF line endings (Git Bash on Windows) -.dockerignore build context for simulator images (repo root) -``` - ## Layers and projects Clean Architecture with the hosts as the presentation layer (driving adapters + composition root). @@ -200,21 +113,3 @@ partition. Notify-then-fetch makes stale or reordered events harmless: each work Deliberate: parking every event would only create a replay job. - Unclassified exceptions are retried 5 times per handler invocation, then parked. The limit is counted in memory and does not cover a failure that kills the process. - -## ADRs - -Written ADRs are in `docs/adr/` (index: `docs/adr/README.md`). This table summarises all of them. - -| ADR | Decision | -|---|---| -| [001](adr/001-project-structure.md) | Project structure: by context first, then by layer; a project wherever the compiler should enforce a rule; no Common project | -| [002](adr/002-event-hubs-over-service-bus.md) | Event Hubs over Service Bus: replay, independent consumers; cost: no DLQ → parking lot and failure classification | -| [003](adr/003-notify-then-fetch.md) | Notify-then-fetch: event is a signal, API state is the truth (ordering, partial payloads, unclear redelivery) | -| [004](adr/004-transactional-outbox.md) | Transactional outbox in Ingress: persist before 200, a single relay publishes; cost: 503 when SQL is down loses the event | -| [005](adr/005-idempotency.md) | Idempotency: idempotent effects, per-worker inbox, checkpoint after processing, stored LLM results | -| [006](adr/006-partition-key-candidate-id.md) | Partition key = `candidateId`, consequences for scaling and the job application payload assumption | -| [007](adr/007-llm-enrichment.md) | LLM enrichment: own port over `IChatClient` (Claude via the Anthropic SDK), PII masking, validation, short retries in the provider SDK inside our timeout, fallback to `NotEnriched`, enrichment inline in the handler | -| [008](adr/008-source-of-truth-and-sync-direction.md) | Source of truth and sync direction: one way from Teamtailor, ownership per field, full replace of the fields we own (last write wins) | -| [009](adr/009-prompt-evaluation.md) | Offline evaluation of the enrichment prompt: golden set of fictional CVs, metrics in code, report per `(PromptVersion, GoldenSetHash)`, run manually never in CI; a CI test only checks that a report exists for the current pair | -| [010](adr/010-observability.md) | Observability: OpenTelemetry over OTLP, Aspire Dashboard locally, one `Infrastructure.Observability` project referenced by the hosts, trace context propagated explicitly through the outbox and `EventData` properties, one consumer span per attempt | -| [011](adr/011-kubernetes-deployment.md) | Kubernetes deployment: only our hosts in the cluster (kind locally), infrastructure in docker compose, one Helm chart, relay switched by `OutboxRelay:Enabled` and run as a one-replica StatefulSet, migrations as a Helm hook Job before any host rolls out | diff --git a/docs/diagrams.md b/docs/diagrams.md index d66b662..116e477 100644 --- a/docs/diagrams.md +++ b/docs/diagrams.md @@ -1,6 +1,7 @@ # TalentSync diagrams -For humans. Descriptive: `docs/architecture.md` is the reference for containers, layout and the key flow. +For humans. Descriptive: `docs/architecture.md` is the reference for containers and the key flow, +`docs/repository-layout.md` for the layout. Keep these diagrams true when a container or the flow changes. ## Containers diff --git a/docs/repository-layout.md b/docs/repository-layout.md new file mode 100644 index 0000000..1f29904 --- /dev/null +++ b/docs/repository-layout.md @@ -0,0 +1,90 @@ +# TalentSync repository layout + +Descriptive: once code exists, the code is current. Keep this file true when a project, folder or tool is added, +moved or removed. Dependency rules between these projects: `docs/architecture.md` (normative). + +``` +README.md for humans: overview, running locally, simplifications +TalentSync.slnx solution, folders mirror the directories below +global.json SDK pin, test runner = Microsoft.Testing.Platform +dotnet-tools.json local tool manifest: dotnet-ef (migrations, used by deploy/run-local.sh) +Directory.Build.props net10.0, nullable, implicit usings, warnings as errors +Directory.Packages.props central package versions (CPM) +src/ + Shared/ + TalentSync.Contracts event envelope, event names, SchemaVersion. References nothing + TalentSync.Recruitment.Domain our read-only model of recruitment: Candidate, JobApplication, + ApplicationStatus, FetchResult (Found | Deleted | Rejected), + port IRecruitmentSource. References nothing + TalentSync.Infrastructure.Teamtailor ACL: TeamtailorRecruitmentSource (port adapter) over TeamtailorApiClient + (HTTP, status mapping) and TeamtailorMapper, internal JSON:API DTOs, 429 handling, + stage mapping from config, 404 → Deleted, 4xx / unmappable → Rejected, + webhook parsing, signature v2 endpoint filter, + public AddTeamtailorWebhooks() (Ingress) + and AddTeamtailorApi() (workers). → Recruitment.Domain + TalentSync.Infrastructure.Observability AddObservability(): OpenTelemetry traces, metrics and logs over OTLP, + subscribes to every TalentSync.* ActivitySource and Meter, HTTP and SQL + instrumentation, SpanExportPolicy (ADR 010). + Only hosts reference it. References nothing in src/ + TalentSync.Infrastructure.Messaging EventProcessorClient hosting, consumer handler skeleton (retry loop, + failure classification), HandlerResult, Inbox and ParkingLot entities + with schema-parameterised EF config. → Contracts + Ingress/ + TalentSync.Ingress.Api host: webhook endpoint, envelope, IngressDbContext + Outbox, relay and + EventHubProducerClient (only when OutboxRelay:Enabled), /health/live. + → Contracts, Infrastructure.Teamtailor + Sync/ + TalentSync.Sync.Application UseCases/SyncCandidateToCrmUseCase, Ports/ICrm. → Recruitment.Domain + TalentSync.Sync.Infrastructure CrmClient + resilience, SyncDbContext. → Sync.Application, Infrastructure.Messaging + TalentSync.Sync.Worker host: candidate.* handler (use case result → HandlerResult), + composition root, group "sync-worker". Web host: only /health/live + (liveness for an orchestrator, no dependency checks) + Matching/ + TalentSync.Matching.Domain ConsultantProfile, CandidateId, Skill, Confidence, Seniority, + Enrichment (+ TryCreate → EnrichmentValidation), EnrichmentStatus. References nothing + TalentSync.Matching.Application UseCases/CreateConsultantProfileUseCase, Ports/IMatchingPlatform, + IEnrichmentService, IEnrichmentResultStore, CvHash. + → Matching.Domain, Recruitment.Domain + TalentSync.Matching.Infrastructure Llm/ (CvMasker, EnrichmentPrompt, ChatEnrichmentService over IChatClient, + Fake/FakeChatClient), + EnrichmentResults/, MatchingPlatformClient, MatchingDbContext with + EnrichmentResultStore. → Matching.Application, Infrastructure.Messaging + TalentSync.Matching.Worker host: job_application.* handler (use case result → HandlerResult), + composition root, group "matching-worker". Web host: only /health/live + (liveness for an orchestrator, no dependency checks) +simulators/ fake external systems, NOT part of the product (see simulators/CLAUDE.md). + FakeTeamtailor No references to src/. + FakeCrm + FakeMatchingPlatform +tests/ + TalentSync.Matching.Domain.Tests + TalentSync.Sync.Application.Tests use case: fetch result and CRM result → use case result + TalentSync.Matching.Application.Tests use case: qualified / not qualified / deleted / rejected, stored or new + enrichment (stored before the upsert), failure → NotEnriched + TalentSync.Matching.Infrastructure.Tests Matching Platform client: request body, status mapping, resilience + TalentSync.Infrastructure.Teamtailor.Tests webhook parsing, stage mapping, 404 → Deleted, 4xx → Rejected + TalentSync.Infrastructure.Messaging.Tests consumer skeleton: inbox / parking lot mapping, failure classification, handler loop + TalentSync.Infrastructure.Observability.Tests request span is a root with the SDK enabled, SpanExportPolicy + TalentSync.Sync.Infrastructure.Tests CRM client: status mapping, 404 on delete = done, resilience + TalentSync.Matching.Evaluation.Tests prompt evaluation: metrics, golden set hash, case files, report + TalentSync.IntegrationTests acceptance scenarios (see .claude/rules/testing.md) +eval/ offline evaluation of the enrichment prompt (ADR 009), never run in CI + README.md what each metric catches, how to read a report + TalentSync.Matching.Evaluation console runner over ChatEnrichmentService. → Matching.Infrastructure + cases/ golden set: one JSON per fictional CV + reports/ committed reports; local/ is gitignored + run-eval.sh runs the evaluation locally; provider, runs, output and model as variables at the top +deploy/ docker-compose.yml (infra + simulators + Aspire Dashboard), simulator.Dockerfile, + eventhubs/Config.json, run-local.sh (containers, migrations, the three hosts), + host.Dockerfile (one image per host), migrations.Dockerfile + migrate.sh + (idempotent EF scripts applied with sqlcmd, ADR 011) +docs/ domain.md, architecture.md, repository-layout.md, acceptance-scenarios.md, diagrams.md, + adr/ (index: adr/README.md), e2e.http (end-to-end demo by hand), + images/ (screenshots for the README) +.github/workflows/ci.yml on PRs and main: Build job, then Test job ("Test results" check, coverage summary) +.claude/rules/ topic rules, some load only for matching paths +.claude/settings.json permissions and hooks enforced by the harness +.claude/hooks/ check-rules.sh: Teamtailor in Domain/Application, direct clock in src/ +.gitattributes *.sh keeps LF line endings (Git Bash on Windows) +.dockerignore build context for simulator images (repo root) +```