Compare commits

..
38 Commits
Author SHA1 Message Date
Rafał Tyński 67f4925bd3 chore(frontend): replace favicon with new TarninaHub mark 2026-10-04 05:42:48 +02:00
Rafał Tyński c23fa95640 docs(readme): document dev ports, TLS certs and default proxy 2026-10-04 05:42:48 +02:00
Rafał Tyński 69af054774 chore(dev): add HTTPS Vite dev server and HTTP launch profile
Serve Vite over HTTPS on :7081 using a certificate exported from dotnet dev-certs (scripts/dev-certs.ps1). Add an http launch profile on :5078 as the default backend and point the /api proxy at it, so dev runs on defaults without env vars.
2026-10-04 05:42:35 +02:00
Rafał Tyński e176d0c211 feat(client): add English default and Ukrainian language with switcher
Default language is English; detect the browser language (Polish -> pl, Ukrainian -> uk, otherwise en) and persist the user choice. Add full Ukrainian (Cyrillic) translations, a language switcher showing native endonyms, and keep vue-i18n, Vuetify locale, <html lang> and document.title in sync.
2026-10-04 04:12:36 +02:00
Rafał Tyński 9854133ac0 feat(client): add high-contrast theme option to theme switcher
Extend theme modes with an explicit high-contrast choice (WCAG 1.4.3) and expose i18n label keys for the theme menu.
2026-10-04 04:04:54 +02:00
Rafał Tyński 804ad617ad feat(client): add app skeleton with routing, state and i18n
Add vue-router routes with accessible navigation (document title + focus management), Pinia session store, vue-i18n (PL/EN) and the default layout. Replace the landing page with routed views and remove the old MainView.
2026-10-04 04:04:35 +02:00
Rafał Tyński 92a904b8a9 feat(client): add accessible design system and API client foundation
Add design tokens, base components (page header, section, status chip, empty state, loading, announcer), a11y composables (text scaling, announcements), high-contrast theme and a typed HTTP client with ProblemDetails error mapping.
2026-10-04 04:04:31 +02:00
Rafał Tyński 4b0225008c chore(client): add lint, format and test tooling
Configure ESLint 10 (flat, Vue + a11y), Prettier, Vitest + @vue/test-utils + jsdom, and add npm scripts for lint/format/test. Document VITE_API_BASE_URL.
2026-10-04 04:04:26 +02:00
Rafał Tyński cca20a9965 test(backend): cover step 17 modules and extract stubs
Add tests for partnership workflow, middleman adaptation and idea notifications; extract reusable test stubs into separate files; add architecture guard forbidding object-valued dictionaries in data contracts.
2026-10-04 03:49:51 +02:00
Rafał Tyński fa1199539b feat(backend): add communication, tester, admin and middleman modules
Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
2026-10-04 03:49:45 +02:00
Rafał Tyński f4f3b3896a docs(backend): document C# conventions in AGENTS.md
Add C# language conventions: one type per file, file-scoped namespaces, sealed/internal types, DTO as records, ban on object-valued dictionaries with the enforcing test, layering, naming and formatting rules.
2026-10-04 03:49:39 +02:00
Rafał Tyński ded596d872 test(ideas): cover workflow, application window and controller outcomes
Add unit tests for idea status transitions, granted application windows and applications controller status mapping (409 when call closed, 201 on success).
2026-10-04 03:13:50 +02:00
Rafał Tyński 127ca92f63 feat(ideas): add idea creator, grant calls and applications
Implement the idea creator and application generator: idea status workflow, social canvas card, grant call windows with 409 when closed, and an LLM-backed idea assistant with offline fallback.
2026-10-04 03:13:45 +02:00
Rafał Tyński fd408b102b test(backend): add matchmaking LLM scoring and limiter tests 2026-10-04 02:52:45 +02:00
Rafał Tyński 0c10489a5e chore(config): configure LLM provider and cost limits 2026-10-04 02:52:45 +02:00
Rafał Tyński 03e2d66ca4 feat(backend): add LLM provider abstraction, contextual ranking and explanations
Add a pluggable LLM provider with an offline template fallback, prompt builder, contextual match scoring (area/location boosts, availability), per-solution explanations and in-memory request limiting for cost control.
2026-10-04 02:52:45 +02:00
Rafał Tyński 182c26d9f8 test(backend): add matchmaking relevance and ranking tests 2026-10-04 02:44:03 +02:00
Rafał Tyński 25ce901021 feat(backend): add semantic matchmaking with embedding pipeline
Add a pluggable embedding provider with an offline deterministic default, vector indexing for problems and innovations, cosine semantic ranking, similar cases and solution suggestions, area filtering and an admin reindex endpoint. Refresh the index when innovations or submissions change.
2026-10-04 02:44:03 +02:00
Rafał Tyński 2bc083fb95 feat(sql): add problem embedding vector index 2026-10-04 02:44:03 +02:00
Rafał Tyński 2676240440 test(backend): add embedding text and innovation stage tests 2026-10-04 02:32:33 +02:00
Rafał Tyński bad37a6ab9 feat(backend): add knowledge base, innovation library and trends modules
Add knowledge item and innovation CRUD with search/filtering, published-only visibility for non-staff, admin publish/update, challenge dictionary, problem aggregation with trends (admin only) and canonical embedding text preparation for matchmaking.
2026-10-04 02:32:33 +02:00
Rafał Tyński fee137aee6 test(backend): add submission workflow, keyword and controller tests 2026-10-04 02:22:05 +02:00
Rafał Tyński cadc321d96 feat(backend): add problem/need submission module with workflow and events
Add submission CRUD with pagination and role scoping, challenge/location binding, automatic keyword extraction, a validated status workflow, and domain events for matchmaking and notifications. Create a problem record together with each submission.
2026-10-04 02:22:00 +02:00
Rafał Tyński 58cc2e4096 docs(readme): document authentication and RBAC 2026-10-04 02:12:00 +02:00
Rafał Tyński 5d3be19ca8 chore(config): document auth signing key and ignore surreal history 2026-10-04 02:12:00 +02:00
Rafał Tyński c163db3029 test(backend): add auth token and security policy tests 2026-10-04 02:12:00 +02:00
Rafał Tyński dcaa1a77db fix(sql): make audit_log context field flexible
Allow nested keys in audit_log.context (SCHEMAFULL rejected free-form context like user.read/consent.grant metadata).
2026-10-04 02:12:00 +02:00
Rafał Tyński 482a4b41f0 feat(backend): add JWT authentication, RBAC and consent/audit services
Add signup/signin with backend-issued JWT (Argon2 passwords in SurrealDB), role-based authorization policies, per-user data access levels, consent register and read audit log. Protect user endpoints and record sensitive reads.
2026-10-04 02:11:56 +02:00
Rafał Tyński 24c9e582c9 ci: update commented CI format and test steps 2026-10-04 01:50:01 +02:00
Rafał Tyński 40e07bc1a6 docs(readme): document layered backend and API endpoints 2026-10-04 01:50:01 +02:00
Rafał Tyński a9815b54e0 fix(infra): fix Dockerfile restore and build script solution name
Copy the layered project files before restore so the Docker build resolves project references, and build the correct solution file (TarninaHub.slnx) in scripts/build.mjs.
2026-10-04 01:50:01 +02:00
Rafał Tyński 1cbf965657 test(backend): add xUnit tests for the API skeleton
Add tests/TarninaHub.Tests with health controller and SurrealDB endpoint builder tests, and register the test and layer projects in the solution.
2026-10-04 01:49:56 +02:00
Rafał Tyński b3756238a3 feat(backend): restructure API into layers with OpenAPI, versioning and error handling
Split the backend into API host, Domain, Application and Infrastructure projects. Add Swagger/OpenAPI per API version, Asp.Versioning (query/header), ProblemDetails with a global exception handler and SurrealQL migrations applied at startup (namespace/database ensured). Infrastructure owns the SurrealDB client, settings and DI.
2026-10-04 01:49:51 +02:00
Rafał Tyński 0a5ef715eb feat(sql): add demo schema additions and synthetic seed data
Add additive schema for demo (user.display_name, innovation.tags, FLEXIBLE free-form fields) and a fully synthetic seed with organizations, personas, problems, submissions, innovations, knowledge items, grant calls, ideas and tester feedback. Verified on SurrealDB 3.2.4: migrations apply cleanly and are idempotent.
2026-10-04 01:24:18 +02:00
Rafał Tyński 6df1027699 feat(sql): add SurrealDB schema and taxonomy seed migrations
Add an idempotent SurrealDB 3.x schema for all domain entities with indexes (HNSW vector and full-text), record access and permissions, plus a taxonomy seed with the 8 social challenges, grouped tags and sample locations. Verified on SurrealDB 3.2.4: migrations apply cleanly, re-run is idempotent and CRUD works.
2026-10-04 01:11:33 +02:00
Rafał Tyński 67ec85b6cc docs(readme): document CI/CD, environments and observability 2026-10-04 00:57:40 +02:00
Rafał Tyński 70fb592cf7 chore(config): add backend environment variable example 2026-10-04 00:57:40 +02:00
Rafał Tyński 6c78351a95 ci: add disabled Gitea Actions CI/CD workflows
Add CI and CD workflows for backend, frontend and image publishing. The workflows are fully commented out because no Gitea Actions runner is attached; uncomment once a runner is registered.
2026-10-04 00:57:40 +02:00
298 changed files with 13589 additions and 388 deletions
+39
View File
@@ -0,0 +1,39 @@
# TarninaHub — backend runtime configuration example.
# Copy to `.env` (not tracked) or provide as environment variables.
# Never commit real secrets.
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:8080
# Auth — JWT signing key (min. 32 chars). REQUIRED in Production; never commit a real value.
# Auth__SigningKey=
# Auth__Issuer=tarninahub
# Auth__Audience=tarninahub
# Auth__TokenLifetimeMinutes=60
# SurrealDB — embedded RocksDB by default.
SurrealDb__Namespace=hubmi
SurrealDb__Database=hubmi
SurrealDb__DataDirectory=data
# SurrealDB — remote mode (optional; leave DataDirectory unused when set).
# SurrealDb__RemoteUrl=ws://127.0.0.1:8010/rpc
# SurrealDb__Username=
# SurrealDb__Password=
# LLM (F-8, matchmaking) — runtime secret only.
# LLM__ApiKey=
# LLM provider/cost control (optional; disabled by default -> offline template rationales).
# Llm__Enabled=false
# Llm__Provider=openai
# Llm__BaseUrl=https://api.openai.com/v1
# Llm__Model=gpt-4o-mini
# Llm__MaxTokens=256
# Llm__MaxRequestsPerMinute=30
# Llm__TimeoutSeconds=20
# Observability (optional).
# Logging__LogLevel__Default=Information
# OTEL_EXPORTER_OTLP_ENDPOINT=
# OTEL_SERVICE_NAME=tarninahub
+50
View File
@@ -0,0 +1,50 @@
# CD workflow — DISABLED.
# No Gitea Actions runner is attached to this repository yet, so the workflow
# below is fully commented out to avoid failing runs. Uncomment everything once
# a runner is registered (repository Settings -> Actions -> Runners).
# name: CD
#
# on:
# push:
# branches: [main]
# tags: ["v*"]
#
# jobs:
# publish:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Read version
# id: version
# run: |
# VERSION=$(sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' backend/TarninaHub.csproj | head -1)
# echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# - name: Log in to registry
# run: echo "${{ secrets.GITEA_TOKEN }}" | docker login git.rtynski.pl -u "${{ github.actor }}" --password-stdin
# - name: Build image
# run: |
# docker build \
# --build-arg APP_VERSION=${{ steps.version.outputs.version }} \
# -t git.rtynski.pl/rtynski/hy2026:${{ steps.version.outputs.version }} \
# -t git.rtynski.pl/rtynski/hy2026:latest .
# - name: Push image
# run: |
# docker push git.rtynski.pl/rtynski/hy2026:${{ steps.version.outputs.version }}
# docker push git.rtynski.pl/rtynski/hy2026:latest
#
# # Deploy demo runs after the image is published from `main`.
# # Configure DEPLOY_HOST / DEPLOY_USER / DEPLOY_SSH_KEY as CI secrets.
# # deploy:
# # needs: publish
# # if: github.ref == 'refs/heads/main'
# # runs-on: ubuntu-latest
# # steps:
# # - name: Deploy over SSH
# # run: |
# # echo "${{ secrets.DEPLOY_SSH_KEY }}" > key && chmod 600 key
# # ssh -i key -o StrictHostKeyChecking=no \
# # "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
# # "cd /opt/tarninahub && APP_VERSION=latest docker compose pull && docker compose up -d"
# # - name: Smoke test
# # run: curl -fsS "http://${{ secrets.DEPLOY_HOST }}:8075/api/health"
+47
View File
@@ -0,0 +1,47 @@
# CI workflow — DISABLED.
# No Gitea Actions runner is attached to this repository yet, so the workflow
# below is fully commented out to avoid failing runs. Uncomment everything once
# a runner is registered (repository Settings -> Actions -> Runners).
# name: CI
#
# on:
# push:
# branches: [main]
# pull_request:
# branches: [main]
#
# jobs:
# backend:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-dotnet@v4
# with:
# dotnet-version: "10.0.x"
# - name: Restore
# run: dotnet restore backend/TarninaHub.csproj
# - name: Build
# run: dotnet build backend/TarninaHub.csproj --configuration Release --no-restore
# - name: Format check
# run: dotnet format whitespace --verify-no-changes TarninaHub.slnx
# - name: Test
# run: dotnet test TarninaHub.slnx --configuration Release --no-build
#
# frontend:
# runs-on: ubuntu-latest
# defaults:
# run:
# working-directory: client
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version: "22"
# cache: npm
# cache-dependency-path: client/package-lock.json
# - name: Install
# run: npm ci
# - name: Build (typecheck + bundle)
# run: npm run build
# # Lint: enabled once ESLint is wired up (`npm run lint`).
+3
View File
@@ -35,3 +35,6 @@ backend/local-docs/
# Docker — lokalne nadpisania
docker-compose.override.yaml
docker-compose.override.yml
# Artefakty narzędzi (np. historia REPL `surreal sql`)
history.txt
+3
View File
@@ -12,6 +12,9 @@ RUN npm run build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY backend/TarninaHub.csproj ./backend/
COPY backend/Domain/TarninaHub.Domain.csproj ./backend/Domain/
COPY backend/Application/TarninaHub.Application.csproj ./backend/Application/
COPY backend/Infrastructure/TarninaHub.Infrastructure.csproj ./backend/Infrastructure/
RUN dotnet restore ./backend/TarninaHub.csproj
COPY backend/ ./backend/
COPY --from=client /src/client/dist ./backend/wwwroot/
+57 -5
View File
@@ -19,24 +19,34 @@ Platforma Małopolskiego Hubu Innowacji Społecznych (MVP hackathonu).
Windows nie kojarzy domyślnie plików `.ps1`, dlatego skrypty uruchamiaj przez
`pwsh` (np. `pwsh ./scripts/docker-push.ps1`), a nie przez dwuklik / samo
`./scripts/docker-push.ps1`. Instalacja: https://aka.ms/powershell
(PowerShell 7.6.6: https://github.com/powershell/powershell/releases#release-v7.6.6)
## Praca lokalna
Backend (dev) nasłuchuje na `https://localhost:7080`.
Backend (dev) domyślnie nasłuchuje na `http://localhost:5078` (profil `http`), a opcjonalnie na `https://localhost:7080` (profil `https`). Dev-serwer Vite działa po HTTPS na `https://localhost:7081` i przekierowuje `/api` do backendu (`http://localhost:5078`).
Certyfikat HTTPS dla Vite pochodzi z `dotnet dev-certs` — wygeneruj go raz (wymaga `pwsh`):
```pwsh
# terminal 1 — backend
pwsh ./scripts/dev-certs.ps1 # lub: npm --prefix client run certs
```
```pwsh
# terminal 1 — backend (profil http, port 5078)
dotnet run --project backend
# terminal 2 — client (Vite, proxy /api -> http://localhost:5078)
# terminal 2 — client (Vite, https://localhost:7081, proxy /api -> http://localhost:5078)
npm --prefix client install
npm --prefix client run dev
```
Aby użyć profilu `https` backendu (port 7080), uruchom `dotnet run --project backend --launch-profile https` i zmień `proxy.target` w `client/vite.config.ts` na `https://localhost:7080`. Domyślna konfiguracja nie wymaga żadnych zmiennych środowiskowych.
## Skrypty
```pwsh
npm --prefix client run dev # uruchamia Vite (dev server)
npm --prefix client run dev # uruchamia Vite (dev server, HTTPS :7081)
npm --prefix client run certs # eksport certyfikatu dev do client/.certs (dotnet dev-certs)
npm --prefix client run build # buduje aplikację Vite do client/dist
npm --prefix client run copy # kopiuje client/dist -> backend/wwwroot
node scripts/build.mjs # buduje całość (client + copy + backend)
@@ -84,12 +94,45 @@ kopiuje go do `backend/wwwroot`, buduje backend, buduje obraz Docker i wypycha g
do `git.rtynski.pl/rtynski/hy2026` jako `<wersja>` oraz `latest`. Nadpisanie
sprawdzenia: `-Force`.
## CI/CD
Pipeline działa w **Gitea Actions** (`.gitea/workflows/`):
> **Uwaga:** workflow są zakomentowane w całości — do repozytorium nie jest podpięty runner Gitea. Odkomentuj po zarejestrowaniu runera (Settings → Actions → Runners).
- `ci.yml` — na `push`/PR do `main`: build backendu (.NET 10), `dotnet format --verify-no-changes` oraz build klienta (Node 22, `vue-tsc` + bundle).
- `cd.yml` — na `main`/tag `v*`: build obrazu i push do `git.rtynski.pl/rtynski/hy2026` (`<wersja>` + `latest`); deploy demo opcjonalnie przez sekrety CI.
Sekrety CI: `GITEA_TOKEN` (`write:package`), opcjonalnie `DEPLOY_HOST`/`DEPLOY_USER`/`DEPLOY_SSH_KEY`. Klucz `LLM__ApiKey` przekazujemy wyłącznie na runtime — nigdy do repo ani warstw obrazu.
## Środowiska i konfiguracja
- **dev** — `appsettings.Development.json`, dane w `backend/data`.
- **demo** — obraz z rejestru, `docker-compose.yaml.example`, port `TARNINAHUB_PORT` (8075), wersja `APP_VERSION`.
- **staging** (opcjonalne) — kandydat wydania przed demo.
Konfiguracja przez zmienne środowiskowe (`SurrealDb__*`, `LLM__ApiKey`, `OTEL_*`); szablon: [`.env.example`](.env.example).
## Observability
- Healthcheck: `GET /api/health` (używany też przez Docker healthcheck).
- Logi strukturalne (JSON) do `stdout`/`stderr`, z correlation ID.
- Docelowo: `/metrics` (Prometheus) i OpenTelemetry (traces).
## Struktura
```
TarninaHub/
├─ backend/ # ASP.NET Core Web API + wwwroot (build z client)
├─ backend/ # ASP.NET Core Web API (API host) + wwwroot + migrations
│ ├─ Controllers/ # warstwa API (kontrolery, DTO wejścia/wyjścia)
│ ├─ Errors/ # globalna obsługa błędów (ProblemDetails)
│ ├─ Configuration/ # konfiguracja Swagger/OpenAPI
│ ├─ Domain/ # warstwa domeny (enumy, reguły)
│ ├─ Application/ # DTO i abstrakcje (interfejsy)
│ ├─ Infrastructure/ # SurrealDB, migracje, DI
│ └─ migrations/ # *.surql (aplikowane przy starcie)
├─ client/ # Vue 3 + TS + Vite
├─ tests/TarninaHub.Tests/ # testy jednostkowe (xUnit)
├─ scripts/
│ ├─ build.mjs # build całości
│ └─ copy-client.mjs # client/dist -> backend/wwwroot
@@ -99,3 +142,12 @@ TarninaHub/
├─ TarninaHub.slnx
└─ README.md
```
## API
- Swagger UI: `GET /swagger` (dokument per wersja, np. `/swagger/v1/swagger.json`).
- Wersjonowanie: `?api-version=1.0` lub nagłówek `X-Api-Version` (domyślnie `1.0`).
- Błędy: `ProblemDetails` (RFC 7807), globalny handler wyjątków.
- Healthcheck: `GET /api/health` (bez wersji, dla infrastruktury).
- Migracje SurrealQL są aplikowane automatycznie przy starcie (katalog `migrations/`).
- Uwierzytelnianie: JWT Bearer (`POST /api/auth/signup`, `POST /api/auth/signin`), role `user`/`jst`/`operator`/`manager`/`admin` i polityki RBAC. Hasła (Argon2) i zgody w SurrealDB; różne poziomy dostępu (`by-consent`/`temporary`/`anonymized`) + rejestr odczytu (`audit_log`).
+6
View File
@@ -1,5 +1,11 @@
<Solution>
<Folder Name="/backend/">
<Project Path="backend/TarninaHub.csproj" />
<Project Path="backend/Application/TarninaHub.Application.csproj" />
<Project Path="backend/Domain/TarninaHub.Domain.csproj" />
<Project Path="backend/Infrastructure/TarninaHub.Infrastructure.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/TarninaHub.Tests/TarninaHub.Tests.csproj" />
</Folder>
</Solution>
+15 -2
View File
@@ -1,6 +1,6 @@
# AGENTS.md — Standardy warstwy Controller
# AGENTS.md — Założenia i standardy backendu (C#)
Dotyczy projektu `backend` (ASP.NET Core Web API, .NET 10).
Dotyczy projektu `backend` oraz testów `tests/TarninaHub.Tests` (ASP.NET Core Web API, .NET 10).
## Komunikaty commitów
@@ -8,6 +8,19 @@ Dotyczy projektu `backend` (ASP.NET Core Web API, .NET 10).
- Zasada obowiązuje **w obu projektach**: repozytorium `DevHeyah` oraz `20_projects/TarninaHub`.
- Stosujemy konwencję Conventional Commits (typ: `feat`, `fix`, `docs`, `chore` itd.).
## Założenia języka C#
- **Platforma:** .NET 10 (`net10.0`), `Nullable enable`, `ImplicitUsings enable`. Nie wyłączaj nullowalności; unikaj operatora `!` (jeśli musisz — uzasadnij).
- **Jeden typ = jeden plik:** każdy typ najwyższego poziomu (klasa, `record`, interfejs, `enum`, `struct`) w osobnym pliku, nazwa pliku zgodna z nazwą typu. Nie zagnieżdżaj typów pomocniczych poza przypadkami prywatnych szczegółów implementacji.
- **Przestrzeń nazw:** wyłącznie file-scoped (`namespace X;`). Kolejność: `using` → `namespace` → typy. Grupuj `using` bez zbędnych duplikatów.
- **Typy:** klasy usług i kontrolerów oznaczaj `sealed`; modele wierszy i typy wewnętrzne jako `internal`. DTO to `public sealed record` (niemutowalne, pozycyjne).
- **Zakaz map z wartościami `object`:** w DTO, interfejsach, `Domain` i modelach `*Row` nie stosuj `Dictionary<string, object>` ani `IReadOnlyDictionary<string, object>`. Elastyczne dane (`content`, `payload`, `contact`, `media`, `attachments`) to mapy wartości tekstowych (`IReadOnlyDictionary<string, string?>`), materializowane do konkretnych `Dictionary<string, string?>` przed przekazaniem do `RawQuery`. Powód: błąd deserializacji CBOR (Dahomey) i nieobsługiwana serializacja typów interfejsowych. Egzekwuje test `NoObjectValuedDictionariesTests`.
- **Wzorce:** promuj `record`, pattern matching, wyrażenia (`=>`), kolekcje `[]`; unikaj `null`-owych kolekcji (inicjalizuj puste).
- **Warstwy:** `Domain` (reguły, enumy), `Application` (DTO, abstrakcje, mapowania), `Infrastructure` (persistence, integracje), `Controllers` (tylko HTTP). Zależności kieruj do wewnątrz (Infrastructure → Application → Domain).
- **Nazewnictwo:** typy/metody/właściwości `PascalCase`, parametry i zmienne lokalne `camelCase`, pola prywatne `_camelCase`, interfejsy `I...`, typy generyczne `T...`. Metody asynchroniczne z sufiksem `Async`.
- **Formatowanie:** zgodnie z `.editorconfig` (C#: 4 spacje, LF, final newline, bez trailing whitespace). Weryfikacja: `dotnet format whitespace --verify-no-changes TarninaHub.slnx` i `dotnet format style --verify-no-changes --severity warn TarninaHub.slnx`.
- **Zmienne:** preferuj `var`, gdy typ jest oczywisty; jawne typy przy niejasności.
## Język nazewnictwa
- Wszystkie nazwy zmiennych, parametrów, pól, metod, klas i plików są **wyłącznie w języku angielskim**.
@@ -0,0 +1,18 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IAdminService
{
Task<IReadOnlyList<SubmissionResponse>> ListSubmissionsAsync(
string? status,
string? challengeId,
CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> VerifyKnowledgeItemAsync(
string knowledgeItemId,
VerifyKnowledgeItemRequest request,
CancellationToken cancellationToken);
Task<AdminMetricsResponse> GetMetricsAsync(CancellationToken cancellationToken);
}
@@ -0,0 +1,12 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public enum CreateApplicationOutcome
{
Created,
CallNotFound,
CallNotOpen,
IdeaNotFound,
}
@@ -0,0 +1,8 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public sealed record CreateApplicationResult(
CreateApplicationOutcome Outcome,
ApplicationResponse? Application);
@@ -0,0 +1,18 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IApplicationService
{
Task<CreateApplicationResult> CreateAsync(
string userId,
CreateApplicationRequest request,
CancellationToken cancellationToken);
Task<ApplicationResponse?> GetAsync(string applicationId, CancellationToken cancellationToken);
Task<IReadOnlyList<ApplicationResponse>> ListForAuthorAsync(
string authorId,
CancellationToken cancellationToken);
}
@@ -0,0 +1,11 @@
namespace TarninaHub.Application.Abstractions;
public interface IAuditLogService
{
Task RecordAsync(
string? actorId,
string action,
string target,
IReadOnlyDictionary<string, string?> context,
CancellationToken cancellationToken);
}
@@ -0,0 +1,8 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IChallengeService
{
Task<IReadOnlyList<SocialChallengeResponse>> ListAsync(CancellationToken cancellationToken);
}
@@ -0,0 +1,20 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IConsentService
{
Task<ConsentResponse> GrantAsync(
string userId,
GrantConsentRequest request,
CancellationToken cancellationToken);
Task<IReadOnlyList<ConsentResponse>> ListForUserAsync(
string userId,
CancellationToken cancellationToken);
Task<bool> RevokeAsync(
string consentId,
string userId,
CancellationToken cancellationToken);
}
@@ -0,0 +1,16 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IGrantCallService
{
Task<IReadOnlyList<GrantCallResponse>> ListAsync(string? status, CancellationToken cancellationToken);
Task<GrantCallResponse?> GetAsync(string grantCallId, CancellationToken cancellationToken);
Task<GrantCallResponse?> UpdateStatusAsync(
string grantCallId,
string status,
CancellationToken cancellationToken);
}
@@ -0,0 +1,12 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IIdeaAssistant
{
Task<IdeaAssistResponse?> AssistAsync(
string ideaId,
IdeaAssistRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,21 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IIdeaService
{
Task<IdeaResponse> CreateAsync(string authorId, CreateIdeaRequest request, CancellationToken cancellationToken);
Task<IdeaResponse?> GetAsync(string ideaId, CancellationToken cancellationToken);
Task<IReadOnlyList<IdeaResponse>> ListForAuthorAsync(string authorId, CancellationToken cancellationToken);
Task<IReadOnlyList<string>> ListDistinctAuthorIdsAsync(CancellationToken cancellationToken);
Task<IdeaResponse?> UpdateAsync(string ideaId, UpdateIdeaRequest request, CancellationToken cancellationToken);
Task<IdeaCardResponse?> GetCardAsync(string ideaId, CancellationToken cancellationToken);
Task<IdeaCardResponse?> UpdateCardAsync(string ideaId, IdeaCardRequest request, CancellationToken cancellationToken);
}
@@ -0,0 +1,22 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IInnovationService
{
Task<IReadOnlyList<InnovationResponse>> ListAsync(
string? challengeId,
string? stage,
string? query,
bool onlyPublished,
CancellationToken cancellationToken);
Task<InnovationResponse?> GetAsync(string innovationId, CancellationToken cancellationToken);
Task<InnovationResponse> CreateAsync(InnovationRequest request, CancellationToken cancellationToken);
Task<InnovationResponse?> UpdateAsync(
string innovationId,
InnovationRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,24 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IKnowledgeItemService
{
Task<IReadOnlyList<KnowledgeItemResponse>> ListAsync(
string? kind,
string? challengeId,
string? query,
bool onlyPublished,
CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> GetAsync(string knowledgeItemId, CancellationToken cancellationToken);
Task<KnowledgeItemResponse> CreateAsync(KnowledgeItemRequest request, CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> UpdateAsync(
string knowledgeItemId,
KnowledgeItemRequest request,
CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> PublishAsync(string knowledgeItemId, CancellationToken cancellationToken);
}
@@ -0,0 +1,19 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IMatchmakingService
{
Task IndexInnovationAsync(string innovationId, CancellationToken cancellationToken);
Task IndexProblemAsync(string problemId, CancellationToken cancellationToken);
Task IndexSubmissionAsync(string submissionId, CancellationToken cancellationToken);
Task<int> ReindexAsync(CancellationToken cancellationToken);
Task<MatchResponse?> MatchAsync(
string submissionId,
MatchRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,6 @@
namespace TarninaHub.Application.Abstractions;
public interface IMigrationRunner
{
Task ApplyAsync(CancellationToken cancellationToken);
}
@@ -0,0 +1,27 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface INotificationService
{
Task NotifyStaffAsync(
string type,
IReadOnlyDictionary<string, string?> payload,
CancellationToken cancellationToken);
Task NotifyUserAsync(
string userId,
string type,
IReadOnlyDictionary<string, string?> payload,
CancellationToken cancellationToken);
Task<IReadOnlyList<NotificationResponse>> ListForUserAsync(
string userId,
CancellationToken cancellationToken);
Task<NotificationResponse?> MarkReadAsync(
string notificationId,
string userId,
CancellationToken cancellationToken);
}
@@ -1,6 +1,6 @@
using TarninaHub.Models;
using TarninaHub.Application.Dtos;
namespace TarninaHub.Services;
namespace TarninaHub.Application.Abstractions;
public interface IStorageService
{
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Abstractions;
public interface ISubmissionEventPublisher
{
Task PublishAsync(SubmissionEvent submissionEvent, CancellationToken cancellationToken);
}
@@ -0,0 +1,28 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface ISubmissionService
{
Task<SubmissionResponse> CreateAsync(
string authorId,
CreateSubmissionRequest request,
CancellationToken cancellationToken);
Task<PagedResponse<SubmissionResponse>> ListAsync(
string viewerId,
bool isStaff,
string? status,
int page,
int pageSize,
CancellationToken cancellationToken);
Task<SubmissionResponse?> GetAsync(
string submissionId,
CancellationToken cancellationToken);
Task<SubmissionResponse?> UpdateStatusAsync(
string submissionId,
string newStatus,
CancellationToken cancellationToken);
}
@@ -0,0 +1,30 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IThreadService
{
Task<IReadOnlyList<ThreadResponse>> ListForUserAsync(
string userId,
bool isStaff,
CancellationToken cancellationToken);
Task<ThreadResponse?> GetAsync(
string threadId,
string userId,
bool isStaff,
CancellationToken cancellationToken);
Task<ThreadResponse> CreateAsync(
string creatorId,
CreateThreadRequest request,
CancellationToken cancellationToken);
Task<MessageResponse?> AddMessageAsync(
string threadId,
string authorId,
bool isStaff,
CreateMessageRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,9 @@
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Abstractions;
public interface ITokenService
{
TokenResult CreateToken(UserAccount user);
}
@@ -0,0 +1,12 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface ITrendService
{
Task<TrendsResponse> GetAsync(
string? challengeId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken);
}
@@ -0,0 +1,15 @@
using TarninaHub.Application.Dtos;
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Abstractions;
public interface IUserService
{
Task<UserAccount?> FindByPhoneAsync(string phone, CancellationToken cancellationToken);
Task<UserAccount?> GetByIdAsync(string userId, CancellationToken cancellationToken);
Task<UserAccount?> CreateAsync(RegisterRequest request, string role, CancellationToken cancellationToken);
Task<bool> VerifyCredentialsAsync(string phone, string password, CancellationToken cancellationToken);
}
@@ -0,0 +1,8 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IMiddlemanService
{
Task<MiddlemanResponse?> AdaptAsync(MiddlemanRequest request, CancellationToken cancellationToken);
}
@@ -0,0 +1,16 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IPartnershipService
{
Task<IReadOnlyList<PartnershipResponse>> ListForUserAsync(
string userId,
bool isStaff,
CancellationToken cancellationToken);
Task<PartnershipResponse> CreateAsync(
string initiatorId,
CreatePartnershipRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Abstractions;
public sealed record SubmissionEvent(
string Type,
string SubmissionId,
string? ActorId,
string Status,
DateTimeOffset At);
@@ -0,0 +1,16 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface ITestFeedbackService
{
Task<TestFeedbackResponse?> CreateAsync(
string innovationId,
string authorId,
CreateTestFeedbackRequest request,
CancellationToken cancellationToken);
Task<IReadOnlyList<TestFeedbackResponse>> ListForInnovationAsync(
string innovationId,
CancellationToken cancellationToken);
}
@@ -0,0 +1,6 @@
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Abstractions;
public sealed record TokenResult(string AccessToken, DateTimeOffset ExpiresAt);
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record AdminMetricsResponse(
int Submissions,
int Problems,
int Ideas,
int Innovations,
int KnowledgeItems,
int Users,
int OpenGrantCalls,
int Applications);
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record ApplicationResponse(
string Id,
string GrantCallId,
string IdeaId,
IReadOnlyDictionary<string, string?> Content,
string Status,
DateTimeOffset CreatedAt);
@@ -0,0 +1,10 @@
namespace TarninaHub.Application.Dtos;
public sealed record ConsentResponse(
string Id,
string UserId,
string Scope,
DateTimeOffset GrantedAt,
DateTimeOffset? ExpiresAt,
DateTimeOffset? RevokedAt);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateApplicationRequest(
[Required] string GrantCallId,
[Required] string IdeaId,
IReadOnlyDictionary<string, string?>? Content);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateIdeaRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required, StringLength(4000, MinimumLength = 10)] string Description,
IdeaCardRequest? Card);
@@ -0,0 +1,7 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateMessageRequest(
[Required, StringLength(4000, MinimumLength = 1)] string Body);
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreatePartnershipRequest(
[Required, StringLength(200, MinimumLength = 3)] string Subject,
IReadOnlyList<string>? Partners);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateSubmissionRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required, StringLength(4000, MinimumLength = 10)] string Description,
[Required] string ChallengeId,
[Required] string LocationId,
IReadOnlyList<string>? Keywords);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateTestFeedbackRequest(
[Range(1, 5)] int Rating,
string? Comment,
string? Improvements);
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateThreadRequest(
[Required, StringLength(200, MinimumLength = 3)] string Subject,
IReadOnlyList<string>? Participants);
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record GrantCallResponse(
string Id,
string Name,
DateTimeOffset OpensAt,
DateTimeOffset ClosesAt,
string Status,
string? ExternalId);
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Dtos;
public sealed record GrantConsentRequest(string Scope, DateTimeOffset? ExpiresAt);
@@ -0,0 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaAssistRequest(string? Focus);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaAssistResponse(
IReadOnlyList<string> Suggestions,
string ImprovedDescription,
bool FromLlm);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaCardRequest(
string? Essence,
string? Audience,
string? Stage);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaCardResponse(
string Id,
string IdeaId,
string Essence,
string Audience,
string Stage);
+13
View File
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaResponse(
string Id,
string Title,
string Description,
string AuthorId,
string Status,
IdeaCardResponse? Card,
DateTimeOffset? UpdatedAt);
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record InnovationRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[StringLength(2000)] string? Summary,
[StringLength(8000)] string? Description,
[Required] string ChallengeId,
[Required] string Stage,
string? Cost,
IReadOnlyDictionary<string, string?>? Contact,
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Media,
bool Published);
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record InnovationResponse(
string Id,
string Title,
string Summary,
string Description,
string ChallengeId,
string Stage,
string? Cost,
IReadOnlyDictionary<string, string?>? Contact,
IReadOnlyList<IReadOnlyDictionary<string, string?>> Media,
bool Published,
DateTimeOffset? UpdatedAt);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record InstitutionProfile(
string? Type,
string? Area,
IReadOnlyList<string>? Audiences);
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record KnowledgeItemRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required] string Kind,
string? ChallengeId,
string? SourceUrl,
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Attachments,
bool Published);
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record KnowledgeItemResponse(
string Id,
string Title,
string Kind,
string? ChallengeId,
string? SourceUrl,
IReadOnlyList<IReadOnlyDictionary<string, string?>> Attachments,
bool Published,
DateTimeOffset? UpdatedAt);
+4
View File
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Dtos;
public sealed record LoginRequest(string Phone, string Password);
+6
View File
@@ -0,0 +1,6 @@
namespace TarninaHub.Application.Dtos;
public sealed record MatchRequest(
int? MaxResults,
string? ChallengeId);
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record MatchResponse(
IReadOnlyList<SimilarCaseItem> SimilarCases,
IReadOnlyList<SolutionItem> Solutions,
bool Fallback);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record MessageResponse(
string Id,
string ThreadId,
string AuthorId,
string Body,
DateTimeOffset CreatedAt);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record MiddlemanRequest(
[Required] string InnovationId,
InstitutionProfile? InstitutionProfile,
string? Needs);
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record MiddlemanResponse(
string ServiceForm,
IReadOnlyList<string> RequiredResources,
string Rationale,
bool FromLlm);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record NotificationResponse(
string Id,
string Type,
IReadOnlyDictionary<string, string?> Payload,
DateTimeOffset? ReadAt,
DateTimeOffset CreatedAt);
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record PagedResponse<T>(
IReadOnlyList<T> Items,
int Page,
int PageSize,
int Total);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record PartnershipResponse(
string Id,
string InitiatorId,
string Subject,
IReadOnlyList<string> Partners,
string Status);
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Dtos;
public sealed record RegisterRequest(string Phone, string? Email, string Password);
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record SimilarCaseItem(
string Id,
string Title,
double Score);
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record SocialChallengeResponse(
string Id,
string Name,
string Description,
int Order);
+10
View File
@@ -0,0 +1,10 @@
namespace TarninaHub.Application.Dtos;
public sealed record SolutionItem(
string InnovationId,
string Title,
string Stage,
string ChallengeId,
double Score,
string Rationale);
+8
View File
@@ -0,0 +1,8 @@
namespace TarninaHub.Application.Dtos;
public sealed record StorageInfo(
string Engine,
string DataDirectory,
string Namespace,
string Database);
+9
View File
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Dtos;
public sealed class StorageProbe
{
public string Label { get; set; } = "probe";
public DateTimeOffset CreatedAt { get; set; }
}
@@ -0,0 +1,6 @@
namespace TarninaHub.Application.Dtos;
public sealed record StorageProbeResponse(
string Label,
DateTimeOffset CreatedAt);
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Dtos;
public sealed record StorageStatusResponse(
string Engine,
string DataDirectory,
string Namespace,
string Database,
int ProbeCount);
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record SubmissionResponse(
string Id,
string ProblemId,
string Title,
string Description,
string ChallengeId,
string LocationId,
string AuthorId,
string Status,
IReadOnlyList<string> Keywords,
DateTimeOffset CreatedAt,
DateTimeOffset? UpdatedAt);
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record TestFeedbackResponse(
string Id,
string InnovationId,
string AuthorId,
int Rating,
string Comment,
string Improvements,
DateTimeOffset CreatedAt);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record ThreadResponse(
string Id,
string Subject,
IReadOnlyList<string> Participants,
DateTimeOffset CreatedAt,
IReadOnlyList<MessageResponse> Messages);
@@ -0,0 +1,8 @@
namespace TarninaHub.Application.Dtos;
public sealed record TokenResponse(
string AccessToken,
string TokenType,
DateTimeOffset ExpiresAt,
UserResponse User);
+7
View File
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record TrendPoint(
string ChallengeId,
string ChallengeName,
int SubmissionCount);
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record TrendsResponse(
DateTimeOffset From,
DateTimeOffset To,
IReadOnlyList<TrendPoint> Points);
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateApplicationRequest(
IReadOnlyDictionary<string, string?>? Content,
string? Status);
@@ -0,0 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateGrantCallStatusRequest([Required] string Status);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateIdeaRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required, StringLength(4000, MinimumLength = 10)] string Description,
string? Status);
@@ -0,0 +1,7 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateSubmissionStatusRequest(
[Required] string Status);
+9
View File
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Dtos;
public sealed record UserResponse(
string Id,
string Phone,
string? Email,
string Role,
string? OrganizationId);
@@ -0,0 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record VerifyKnowledgeItemRequest(bool Verified);
@@ -0,0 +1,26 @@
using TarninaHub.Domain.Enums;
namespace TarninaHub.Application.Mappings;
public static class IdeaStatusWire
{
public static string ToWire(this IdeaStatus status) => status switch
{
IdeaStatus.Draft => "draft",
IdeaStatus.Submitted => "submitted",
IdeaStatus.InReview => "in_review",
IdeaStatus.Accepted => "accepted",
IdeaStatus.Rejected => "rejected",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
};
public static IdeaStatus? Parse(string? value) => value switch
{
"draft" => IdeaStatus.Draft,
"submitted" => IdeaStatus.Submitted,
"in_review" => IdeaStatus.InReview,
"accepted" => IdeaStatus.Accepted,
"rejected" => IdeaStatus.Rejected,
_ => null,
};
}
@@ -0,0 +1,29 @@
using TarninaHub.Domain.Enums;
namespace TarninaHub.Application.Mappings;
public static class InnovationStageWire
{
private static readonly Dictionary<string, InnovationStage> ByWire = new(StringComparer.Ordinal)
{
["idea"] = InnovationStage.Idea,
["pilot"] = InnovationStage.Pilot,
["scaling"] = InnovationStage.Scaling,
["service"] = InnovationStage.Service,
};
public static bool IsValid(string? value) =>
!string.IsNullOrWhiteSpace(value) && ByWire.ContainsKey(value);
public static string ToWire(this InnovationStage stage) => stage switch
{
InnovationStage.Idea => "idea",
InnovationStage.Pilot => "pilot",
InnovationStage.Scaling => "scaling",
InnovationStage.Service => "service",
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null),
};
public static InnovationStage? Parse(string? value) =>
value is not null && ByWire.TryGetValue(value, out var stage) ? stage : null;
}
@@ -0,0 +1,22 @@
using TarninaHub.Domain.Enums;
namespace TarninaHub.Application.Mappings;
public static class PartnershipStatusWire
{
public static string ToWire(this PartnershipStatus status) => status switch
{
PartnershipStatus.Proposed => "proposed",
PartnershipStatus.Active => "active",
PartnershipStatus.Closed => "closed",
_ => "proposed",
};
public static PartnershipStatus? Parse(string? value) => value?.Trim().ToLowerInvariant() switch
{
"proposed" => PartnershipStatus.Proposed,
"active" => PartnershipStatus.Active,
"closed" => PartnershipStatus.Closed,
_ => null,
};
}
@@ -0,0 +1,36 @@
using TarninaHub.Domain.Enums;
namespace TarninaHub.Application.Mappings;
public static class SubmissionStatusWire
{
public static string ToWire(this SubmissionStatus status) => status switch
{
SubmissionStatus.New => "new",
SubmissionStatus.Triaged => "triaged",
SubmissionStatus.Matched => "matched",
SubmissionStatus.InProgress => "in_progress",
SubmissionStatus.Closed => "closed",
SubmissionStatus.Rejected => "rejected",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
};
public static SubmissionStatus? Parse(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return value switch
{
"new" => SubmissionStatus.New,
"triaged" => SubmissionStatus.Triaged,
"matched" => SubmissionStatus.Matched,
"in_progress" => SubmissionStatus.InProgress,
"closed" => SubmissionStatus.Closed,
"rejected" => SubmissionStatus.Rejected,
_ => null,
};
}
}
@@ -0,0 +1,14 @@
using TarninaHub.Application.Dtos;
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Mappings;
public static class UserMappings
{
public static UserResponse ToResponse(this UserAccount user) => new(
user.Id,
user.Phone,
user.Email,
user.Role,
user.OrganizationId);
}
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Matchmaking;
public static class EmbeddingConstants
{
/// <summary>Embedding dimension; must match the HNSW index definition.</summary>
public const int Dimension = 1536;
}
@@ -0,0 +1,103 @@
using System.Text;
namespace TarninaHub.Application.Matchmaking;
/// <summary>
/// Deterministic, offline embedding provider (feature hashing). Produces
/// L2-normalized vectors so cosine similarity reflects shared vocabulary.
/// Interchangeable with a real LLM provider behind <see cref="IEmbeddingProvider"/>
/// (see ADR-0005). Used as the default until an external provider is wired.
/// </summary>
public sealed class HashingEmbeddingProvider : IEmbeddingProvider
{
public int Dimension => EmbeddingConstants.Dimension;
public Task<float[]> EmbedAsync(string text, CancellationToken cancellationToken)
{
return Task.FromResult(Embed(text));
}
public Task<IReadOnlyList<float[]>> EmbedBatchAsync(
IReadOnlyList<string> texts,
CancellationToken cancellationToken)
{
IReadOnlyList<float[]> result = texts.Select(Embed).ToList();
return Task.FromResult(result);
}
public float[] Embed(string text)
{
var vector = new float[Dimension];
if (string.IsNullOrWhiteSpace(text))
{
return vector;
}
foreach (var token in Tokenize(text))
{
var index = (int)(Fnv1a(token) % Dimension);
vector[index] += 1f;
}
Normalize(vector);
return vector;
}
private static IEnumerable<string> Tokenize(string text)
{
var buffer = new StringBuilder();
foreach (var character in text)
{
if (char.IsLetterOrDigit(character))
{
buffer.Append(char.ToLowerInvariant(character));
}
else if (buffer.Length > 0)
{
yield return buffer.ToString();
buffer.Clear();
}
}
if (buffer.Length > 0)
{
yield return buffer.ToString();
}
}
private static void Normalize(float[] vector)
{
double sumOfSquares = vector.Sum(value => (double)value * value);
var magnitude = Math.Sqrt(sumOfSquares);
if (magnitude <= 0)
{
return;
}
for (var i = 0; i < vector.Length; i++)
{
vector[i] = (float)(vector[i] / magnitude);
}
}
private static uint Fnv1a(string token)
{
const uint offset = 2166136261;
const uint prime = 16777619;
var hash = offset;
foreach (var character in token)
{
hash ^= character;
hash *= prime;
}
return hash;
}
}
@@ -0,0 +1,12 @@
namespace TarninaHub.Application.Matchmaking;
public interface IEmbeddingProvider
{
int Dimension { get; }
Task<float[]> EmbedAsync(string text, CancellationToken cancellationToken);
Task<IReadOnlyList<float[]>> EmbedBatchAsync(
IReadOnlyList<string> texts,
CancellationToken cancellationToken);
}
@@ -0,0 +1,13 @@
namespace TarninaHub.Application.Matchmaking;
public interface ILlmProvider
{
/// <summary>True when a real provider is configured (API key etc.).</summary>
bool IsConfigured { get; }
/// <summary>Returns the completion text or null when unavailable.</summary>
Task<string?> CompleteAsync(
string systemPrompt,
string userPrompt,
CancellationToken cancellationToken);
}
@@ -0,0 +1,10 @@
namespace TarninaHub.Application.Matchmaking;
public interface ILlmRequestLimiter
{
/// <summary>
/// Tries to reserve one LLM request within the current window.
/// Returns false when the limit is exceeded (caller should fall back).
/// </summary>
bool TryAcquire();
}
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Matchmaking;
public sealed record LlmPrompt(string System, string User);
@@ -0,0 +1,32 @@
namespace TarninaHub.Application.Matchmaking;
public static class MatchPromptBuilder
{
private const string SystemPrompt =
"Jesteś asystentem Małopolskiego Hubu Innowacji Społecznych. " +
"Wyjaśnij po polsku, prostym językiem (maks. 2 zdania), dlaczego proponowana " +
"innowacja pasuje do zgłoszonej potrzeby. Nie wymyślaj faktów.";
public static LlmPrompt BuildExplanation(
string problemTitle,
string problemDescription,
string challengeId,
string innovationTitle,
string innovationSummary,
string stage,
double score)
{
var user =
$"Potrzeba: {problemTitle}\n" +
$"Opis: {problemDescription}\n" +
$"Obszar: {challengeId}\n" +
$"Proponowana innowacja: {innovationTitle}\n" +
$"Streszczenie: {innovationSummary}\n" +
$"Etap: {stage}\n" +
$"Ocena dopasowania: {score:0.00}\n" +
"Uzasadnij krótko dopasowanie.";
return new LlmPrompt(SystemPrompt, user);
}
}
@@ -0,0 +1,37 @@
namespace TarninaHub.Application.Matchmaking;
/// <summary>
/// Combines semantic similarity with context signals (area, location) and
/// availability into a final 0..1 match score.
/// </summary>
public static class MatchScoring
{
private const double ChallengeBoost = 0.10;
private const double LocationBoost = 0.05;
public static double Combine(
double semanticScore,
bool challengeMatch,
bool locationMatch,
bool available)
{
if (!available)
{
return 0;
}
var score = semanticScore;
if (challengeMatch)
{
score += ChallengeBoost;
}
if (locationMatch)
{
score += LocationBoost;
}
return Math.Clamp(score, 0, 1);
}
}
@@ -0,0 +1,15 @@
namespace TarninaHub.Application.Matchmaking;
/// <summary>
/// Default provider used when no external LLM is configured. Keeps the system
/// fully offline/deterministic; matchmaking falls back to template rationales.
/// </summary>
public sealed class NullLlmProvider : ILlmProvider
{
public bool IsConfigured => false;
public Task<string?> CompleteAsync(
string systemPrompt,
string userPrompt,
CancellationToken cancellationToken) => Task.FromResult<string?>(null);
}

Some files were not shown because too many files have changed in this diff Show More