There is no JavaScript, TypeScript, or Node.js in Phase 1. Do not create package.json, pnpm-workspace.yaml, turbo.json, apps/, services/, or packages/ directories. These land with Phase 2 when a real web consumer exists.
legacy/pal-v2 is a submodule containing the original PAL v2 PowerShell tool. Do not modify it. It exists solely to inform port decisions.
dotnet/schemas/pal.pack.v1.json is the authoritative pack schema — it supersedes docs/internal/seed-specs/implementation-spec-pack/PAL-Pack-Schema-v1.md (the seeded doc is ChatGPT-generated and was revised). Similarly, dotnet/schemas/pal.report.v1.json supersedes the seeded report schema doc.
See docs/architecture/adr/0001-deviations-from-seed-docs.md for all 12 ratified deviations. The most important:
- No numeric health score: Use tri-state status (critical/warning/healthy), not a 0-100 additive score.
- Declarative comparators: No expression DSL or parser. Every rule condition uses
metric+aggregation+operator+threshold+duration_percent. - snake_case metric IDs: All canonical metric IDs use snake_case (e.g.,
processor.percent_processor_time). Legacy counter paths live inmetric_aliasesin each pack. host_contextin schema v1: RAM-relative and CPU-count-relative thresholds usehost_context.total_physical_memory_mb/host_context.logical_processor_count— not deferred.- Spectre.Console.Cli (not System.CommandLine which is still beta).
- ScottPlot for chart SVGs (not hand-rolled renderer).
- Content-hash IDs: finding_id and report_id are SHA-256-based, not ULID.
All JSON and HTML artifacts use new UTF8Encoding(false). Never use bare Encoding.UTF8 for writing report files.
severity desc → category asc → rule_id asc → finding_id asc. The RuleEngine must enforce this on every run.
If a rule references host_context.total_physical_memory_mb or host_context.logical_processor_count and the value is unknown, emit an informational warning and skip the rule. Do not fail the run.
<input-stem>.pal-report.json and <input-stem>.pal-report.html. Charts go in <output>/charts/<report-name>-<chart-id>.svg.
BLG files are supported via Windows PDH interop (BlgCollector.cs in Pal.Ingestion). On non-Windows platforms, a PlatformNotSupportedException is thrown with the relog -f CSV fallback command. The CollectorFactory dispatches by file extension: .blg → BlgCollector (Windows-only), .csv → CsvCollector.
Packs can be signed using RSA-PSS-SHA256 via pal packs sign --pack <dir> --key <privkey.pem>. The signature is stored as a sidecar pack.yaml.sig adjacent to pack.yaml. Verification is enforced by PackLoader.Load(..., SignatureRequirement.Required, trustedKeys) and pal validate-pack --require-signature --trust-key <pubkey.pem>. See ADR 0003 for format details.
Pack conditions can specify a window: block (requires schema_version: "pal.pack/v1.1") to evaluate aggregations over a rolling window rather than the full series. Supported aggregations: avg, min, max, p90, p95, p99 (not trend). See ADR 0004. PackValidator enforces the version gate.
MarkdownReportWriter in Pal.Reporting/Markdown/ emits findings as GFM tables using the same JsonReportWriter.WriteInput shape as the HTML and JSON writers. Enabled with --markdown on the local CLI (pal analyze), ?format=markdown on the API report endpoint, and --format markdown on pal remote report.
Jobs submitted with includeDataset: true (API) or --include-dataset (CLI) persist a gzip-compressed JSON dataset artifact to data/storage/datasets/<jobId>/dataset.json.gz. The artifact is retrieved via GET /api/workspaces/{id}/analysis/{jobId}/dataset or pal remote dataset <jobId> --output <path>. RetentionWorker cleans it alongside reports.
GET /packs/{id}/versions/{version}/validation runs PackValidator against the stored pack YAML and returns { isValid, errors, warnings }. This is a global (non-workspace-scoped) endpoint reachable as pal remote validate-pack <pack-id> <version>.
AnalysisJobEntity carries BaselineType (nullable text, one of machine | role | workload | release) and BaselineContextJson (nullable text, arbitrary JSON key like {"machine":"WEB-01"}). Both are set via PATCH /api/workspaces/{id}/analysis/{jobId}/baseline with { isBaseline, label, type, contextJson }. Versioning is implicit: multiple baselines sharing the same (type, contextJson) are treated as versions, ordered by CreatedAt desc. GET /analysis/baselines/versions?type=<x>&contextJson=<json> lists all. Reachable as pal remote baselines list [--type]. Submitting with selectedBaselineId in CreateAnalysisRequest triggers auto-compare on job completion via IAutoCompareService.
IDiagnosticsService in Pal.Application/Diagnostics/ generates rule-based DiagnosticInsightDto items for a completed job. Sources: findings (critical/warning), worsening/appearing trends, both-worsening correlation pairs. Every insight cites AffectedRuleIds + optional SourceDirection (no black-box inference). Endpoint: GET /api/workspaces/{id}/analysis/{jobId}/diagnostics. Embedded in JobDetail.razor as a collapsible <details> block. CLI: pal remote diagnostics <job-id>.
/baselines Blazor page lists all designated baselines with type/label/context/packs, inline version history, compare link, and remove action. Type filter dropdown. Nav link between Compare and Trends in MainLayout.razor.
Golden fixture tests use --now <ISO> to override generated_at_utc so the output is byte-identical across runs. ScottPlot SVG tests assert byte-identical output on two renders of the same data.
# Build
dotnet build dotnet/Pal.sln -c Release
# Unit tests (no Docker required)
dotnet test dotnet/Pal.sln -c Release --filter "FullyQualifiedName!~Pal.Api.Tests"
# Integration tests (requires Docker Desktop running locally)
dotnet test dotnet/tests/Pal.Api.Tests -c Release
# Run API locally (postgres must be up)
docker compose up -d postgres
dotnet run --project dotnet/src/Pal.Api
# Run CLI — analysis
dotnet run --project dotnet/src/Pal.Cli -- analyze --input <csv> --output out --pack-dir packs/thresholds
# Sign a pack
dotnet run --project dotnet/src/Pal.Cli -- packs sign --pack packs/thresholds/windows-core --key tools/test-keys/dev.priv.pem
# Validate a pack (with signature enforcement)
dotnet run --project dotnet/src/Pal.Cli -- validate-pack --path packs/thresholds/windows-core --require-signature --trust-key tools/test-keys/dev.pub.pem
# Add an EF Core migration (dotnet-ef is NOT on PATH — use full path)
& "$env:USERPROFILE\.dotnet\tools\dotnet-ef.exe" migrations add <Name> `
--project dotnet/src/Pal.Persistence `
--startup-project dotnet/src/Pal.ApiAll source under dotnet/src/:
| Project | Role |
|---|---|
Pal.Engine |
Core analysis: dataset model, rule evaluator, statistics, status classifier |
Pal.Ingestion |
CSV collector; BLG collector (Windows PDH interop) |
Pal.Packs |
YAML pack loader, validator, pack resolver |
Pal.Reporting |
JSON + HTML report writers, ScottPlot SVG charts |
Pal.Application |
Shared DTOs, interfaces, service contracts |
Pal.Persistence |
EF Core 8 + PostgreSQL — all entities, migrations, repositories |
Pal.Api |
ASP.NET Core minimal API + background workers (AnalysisWorker, RetentionWorker) |
Pal.Cli |
Spectre.Console.Cli standalone tool |
The API uses a two-level hierarchy: Org → Workspace. All data-plane resources carry a WorkspaceId FK enforced by EF Core global query filters and DB-level cascade constraints.
- Route group
/api/workspaces/{workspaceId:guid}runsTenantResolutionEndpointFilter— validates workspace existence and org membership before any handler runs. - Global query filters use
.GetValueOrDefault()onITenantContext.WorkspaceId(nullable Guid) to avoid an EF parameter-extraction crash. Do not change this to!= null. - Repositories throw
InvalidOperationExceptionwhenWorkspaceIdis null — they must only be called from within the workspace route group.
API uses API-key authentication: Authorization: Bearer <token>. Tokens are SHA-256-hashed before storage (TokenHasher). Use POST /api/tokens to create one. No JWT — do not add JWT middleware.
dotnet efnot on PATH: The EF global tool must be invoked as& "$env:USERPROFILE\.dotnet\tools\dotnet-ef.exe"in PowerShell (or full path in bash).Pal.Api.Testsrequires Docker: Integration tests use Testcontainers (PostgreSQL container). They are excluded from the Windows CI runner. Exclude with--filter "FullyQualifiedName!~Pal.Api.Tests"when Docker is unavailable.DefaultTenant.WorkspaceId: A seeded workspace used by all tests. Test entity factories (MakeJob,MakeUpload, etc.) must setWorkspaceId = DefaultTenant.WorkspaceId— forgetting this causes FK violations once DB-level constraints exist.